diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 3aa0cbe0f..66e10f667 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -1963,10 +1963,14 @@ viewer.totalPages=Total Pages toolPanel.modeToggle.sidebar=Switch to advanced sidebar toolPanel.modeToggle.fullscreen=Switch to legacy fullscreen toolPanel.overlay.title=All tools -toolPanel.overlay.subtitle=Browse and launch tools in the legacy fullscreen catalog. +toolPanel.overlay.subtitle=Browse every tool in the legacy fullscreen catalog. toolPanel.overlay.close=Close toolPanel.overlay.totalLabel_one={{count}} tool available toolPanel.overlay.totalLabel_other={{count}} tools available +toolPanel.overlay.layoutLabel=Layout +toolPanel.overlay.layoutCompact=Compact grid +toolPanel.overlay.layoutDetailed=Detailed cards +toolPanel.overlay.matchedSynonym=Matches "{{text}}" toolPanel.overlayHint=Select a tool to open it in the workspace. toolPanel.modePrompt.title=Choose your tools view toolPanel.modePrompt.description=Preview both layouts and choose how you want to explore Stirling PDF tools. diff --git a/frontend/src/components/tools/ToolPanelOverlay.css b/frontend/src/components/tools/ToolPanelOverlay.css index e46948e46..ecf02eb01 100644 --- a/frontend/src/components/tools/ToolPanelOverlay.css +++ b/frontend/src/components/tools/ToolPanelOverlay.css @@ -17,38 +17,66 @@ .tool-panel-overlay--closing { opacity: 0; - transform: translateX(-10%); + transform: translateX(-6%); pointer-events: none; } .tool-panel-overlay__paper { flex: 1; - max-width: 72rem; + max-width: 96rem; margin: 2rem; display: flex; flex-direction: column; background: var(--bg-background); - border-radius: 1.25rem; + border-radius: 1.5rem; overflow: hidden; + box-shadow: var(--shadow-overlay, 0 24px 64px rgba(15, 23, 42, 0.32)); } .tool-panel-overlay__header { display: flex; justify-content: space-between; align-items: center; - padding: 1.5rem; + padding: 1.75rem; gap: 1rem; border-bottom: 1px solid var(--border-subtle); + background: var(--bg-elevated); } .tool-panel-overlay__search { - padding: 1rem 1.5rem; + display: flex; + gap: 1.25rem; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.75rem; border-bottom: 1px solid var(--border-subtle); background: var(--bg-toolbar); + flex-wrap: wrap; +} + +.tool-panel-overlay__search-input { + flex: 1 1 22rem; + min-width: 16rem; } .tool-panel-overlay__search-input .search-input-container { - width: min(32rem, 100%); + width: 100%; +} + +.tool-panel-overlay__search-controls { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.tool-panel-overlay__layout-toggle { + display: flex; + align-items: center; +} + +.tool-panel-overlay__layout-toggle .mantine-SegmentedControl-control { + font-weight: 500; } .tool-panel-overlay__body { @@ -61,21 +89,160 @@ height: 100%; } -.tool-panel-overlay__results, -.tool-panel-overlay__picker { - padding: 1.5rem; - min-height: 100%; -} - -.tool-panel-overlay__picker { +.tool-panel-overlay__content { + padding: 2rem; display: flex; flex-direction: column; - background: var(--bg-toolbar); - border-radius: 1rem; + gap: 2.5rem; } -.tool-panel-overlay__picker .tool-picker-scrollable { - padding-bottom: 2rem; +.tool-panel-overlay__section { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.tool-panel-overlay__section-header { + display: flex; + align-items: baseline; + gap: 0.5rem; + color: var(--text-strong); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.tool-panel-overlay__grid { + display: grid; + gap: 1.25rem; +} + +.tool-panel-overlay__grid--compact { + grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); +} + +.tool-panel-overlay__grid--detailed { + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); +} + +.tool-panel-overlay__tile-link, +.tool-panel-overlay__tile-button { + all: unset; + display: block; +} + +.tool-panel-overlay__tile-button { + cursor: pointer; +} + +.tool-panel-overlay__tile-button[aria-disabled="true"], +.tool-panel-overlay__tile-link[aria-disabled="true"] { + pointer-events: none; +} + +.tool-panel-overlay__tile { + width: 100%; + height: 100%; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: 1.25rem; + padding: 1.25rem; + display: flex; + gap: 1rem; + color: var(--text-primary); + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.tool-panel-overlay__tile[data-variant="compact"] { + flex-direction: column; + align-items: flex-start; + min-height: 10.5rem; +} + +.tool-panel-overlay__tile[data-variant="detailed"] { + flex-direction: row; + align-items: flex-start; + min-height: 8.75rem; +} + +.tool-panel-overlay__tile[data-selected="true"] { + border-color: var(--accent-primary, var(--mantine-color-pink-6)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 25%, transparent); +} + +.tool-panel-overlay__tile[data-disabled="true"] { + opacity: 0.45; +} + +.tool-panel-overlay__tile:hover:not([data-disabled="true"]) { + transform: translateY(-4px); + border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 35%, var(--border-subtle)); + box-shadow: 0 16px 32px rgba(15, 23, 42, 0.18); +} + +.tool-panel-overlay__tile-icon { + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + border-radius: 0.9rem; + background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 8%, transparent); + color: var(--tools-text-and-icon-color); + flex-shrink: 0; +} + +.tool-panel-overlay__tile-icon svg, +.tool-panel-overlay__tile-icon span { + font-size: 1.9rem; +} + +.tool-panel-overlay__tile-body { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.tool-panel-overlay__tile-name { + font-size: 1rem; + line-height: 1.35; +} + +.tool-panel-overlay__tile-description { + line-height: 1.45; +} + +.tool-panel-overlay__tile-match { + font-size: 0.75rem; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.tool-panel-overlay__tile-hotkey { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: var(--mantine-color-dimmed); +} + +.tool-panel-overlay__empty { + display: flex; + align-items: center; + justify-content: center; + min-height: 12rem; + border: 1px dashed var(--border-subtle); + border-radius: 1.25rem; + background: rgba(148, 163, 184, 0.04); +} + +@media (max-width: 1200px) { + .tool-panel-overlay__paper { + margin: 1.5rem; + } + + .tool-panel-overlay__content { + padding: 1.5rem; + } } @media (max-width: 1024px) { @@ -83,4 +250,19 @@ margin: 0; border-radius: 0; } + + .tool-panel-overlay__header, + .tool-panel-overlay__search { + padding: 1.25rem; + } +} + +@media (max-width: 640px) { + .tool-panel-overlay__grid--compact { + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + } + + .tool-panel-overlay__grid--detailed { + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + } } diff --git a/frontend/src/components/tools/ToolPanelOverlay.tsx b/frontend/src/components/tools/ToolPanelOverlay.tsx index c267b2613..836680d4b 100644 --- a/frontend/src/components/tools/ToolPanelOverlay.tsx +++ b/frontend/src/components/tools/ToolPanelOverlay.tsx @@ -1,21 +1,34 @@ import { useEffect, useMemo, useState } from 'react'; -import { ActionIcon, Badge, Group, Paper, ScrollArea, Text, Tooltip } from '@mantine/core'; +import { ActionIcon, Badge, Group, Paper, ScrollArea, SegmentedControl, Text, Tooltip } from '@mantine/core'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded'; import DashboardCustomizeRoundedIcon from '@mui/icons-material/DashboardCustomizeRounded'; import { useTranslation } from 'react-i18next'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import ToolSearch from './toolPicker/ToolSearch'; -import ToolPicker from './ToolPicker'; -import SearchResults from './SearchResults'; import { ToolId } from '../../types/toolId'; +import { useToolSections } from '../../hooks/useToolSections'; +import NoToolsFound from './shared/NoToolsFound'; +import ToolPanelOverlayTile from './ToolPanelOverlayTile'; +import { getSubcategoryLabel } from '../../data/toolsTaxonomy'; import './ToolPanelOverlay.css'; +type LayoutVariant = 'compact' | 'detailed'; + interface ToolPanelOverlayProps { isOpen: boolean; } const EXIT_ANIMATION_MS = 320; +const LAYOUT_STORAGE_KEY = 'toolPanelOverlayLayout'; + +const getInitialLayout = (): LayoutVariant => { + if (typeof window === 'undefined') { + return 'compact'; + } + const stored = window.localStorage.getItem(LAYOUT_STORAGE_KEY); + return stored === 'detailed' ? 'detailed' : 'compact'; +}; export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { const { t } = useTranslation(); @@ -33,6 +46,14 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { const [shouldRender, setShouldRender] = useState(isOpen); const [isClosing, setIsClosing] = useState(false); + const [layout, setLayout] = useState(getInitialLayout); + + const { sections, searchGroups } = useToolSections(filteredTools, searchQuery); + + useEffect(() => { + if (typeof window === 'undefined') return; + window.localStorage.setItem(LAYOUT_STORAGE_KEY, layout); + }, [layout]); useEffect(() => { if (isOpen) { @@ -69,6 +90,24 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { const showSearchResults = useMemo(() => searchQuery.trim().length > 0, [searchQuery]); const totalToolCount = showSearchResults ? filteredTools.length : Object.keys(toolRegistry).length; + const matchedTextMap = useMemo(() => { + const map = new Map(); + filteredTools.forEach(({ item: [id], matchedText }) => { + if (matchedText) { + map.set(id, matchedText); + } + }); + return map; + }, [filteredTools]); + + const subcategoryGroups = useMemo(() => { + if (showSearchResults) { + return searchGroups; + } + const allSection = sections.find(section => section.key === 'all'); + return allSection ? allSection.subcategories : []; + }, [searchGroups, sections, showSearchResults]); + if (!shouldRender) { return null; } @@ -82,6 +121,8 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { ? t('toolPanel.modeToggle.sidebar', 'Switch to advanced sidebar') : t('toolPanel.modeToggle.fullscreen', 'Switch to legacy fullscreen'); + const layoutLabel = t('toolPanel.overlay.layoutLabel', 'Layout'); + return (
- {t('toolPanel.overlay.subtitle', 'Browse and launch tools in the legacy fullscreen catalog.')} + {t('toolPanel.overlay.subtitle', 'Browse every tool in the legacy fullscreen catalog.')}
@@ -130,44 +171,69 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) {
- -
- + +
+
+
+ setLayout(value as LayoutVariant)} + size="sm" + aria-label={layoutLabel} + data={[ + { label: t('toolPanel.overlay.layoutCompact', 'Compact grid'), value: 'compact' }, + { label: t('toolPanel.overlay.layoutDetailed', 'Detailed cards'), value: 'detailed' }, + ]} />
- {t('toolPanel.overlay.totalLabel', '{{count}} tools available', { + {t('toolPanel.overlay.totalLabel', { count: totalToolCount, + defaultValue: '{{count}} tools available', })} - +
- {showSearchResults ? ( -
- handleToolSelect(id as ToolId)} - searchQuery={searchQuery} - /> -
- ) : ( -
- handleToolSelect(id as ToolId)} - filteredTools={filteredTools} - isSearching={showSearchResults} - /> -
- )} +
+ {subcategoryGroups.length === 0 ? ( +
+ +
+ ) : ( + subcategoryGroups.map(group => ( +
+
+ + {getSubcategoryLabel(t, group.subcategoryId)} + +
+
+ {group.tools.map(({ id, tool }) => ( + handleToolSelect(toolId as ToolId)} + isSelected={selectedToolKey === id} + matchedSynonym={matchedTextMap.get(id)} + /> + ))} +
+
+ )) + )} +
diff --git a/frontend/src/components/tools/ToolPanelOverlayTile.tsx b/frontend/src/components/tools/ToolPanelOverlayTile.tsx new file mode 100644 index 000000000..46823fd95 --- /dev/null +++ b/frontend/src/components/tools/ToolPanelOverlayTile.tsx @@ -0,0 +1,176 @@ +import React, { useMemo } from 'react'; +import { Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; +import { Tooltip } from '../shared/Tooltip'; +import { useToolNavigation } from '../../hooks/useToolNavigation'; +import { handleUnlessSpecialClick } from '../../utils/clickHandlers'; +import { useHotkeys } from '../../contexts/HotkeyContext'; +import HotkeyDisplay from '../hotkeys/HotkeyDisplay'; + +interface ToolPanelOverlayTileProps { + id: string; + tool: ToolRegistryEntry; + layout: 'compact' | 'detailed'; + onSelect: (id: string) => void; + isSelected: boolean; + matchedSynonym?: string; +} + +const ToolPanelOverlayTile: React.FC = ({ + id, + tool, + layout, + onSelect, + isSelected, + matchedSynonym, +}) => { + const { t } = useTranslation(); + const { getToolNavigation } = useToolNavigation(); + const { hotkeys } = useHotkeys(); + + const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; + const binding = hotkeys[id]; + + const navProps = !isUnavailable && !tool.link ? getToolNavigation(id, tool) : null; + + const tooltipContent = useMemo(() => { + if (layout !== 'compact') { + return null; + } + + return ( +
+ {tool.description} + {binding && ( +
+ + {t('settings.hotkeys.shortcut', 'Shortcut')} + + +
+ )} +
+ ); + }, [binding, layout, t, tool.description]); + + const iconNode = useMemo(() => { + if (!tool.icon) { + return null; + } + + if (React.isValidElement(tool.icon)) { + const existingStyle = (tool.icon.props as { style?: React.CSSProperties }).style || {}; + return React.cloneElement(tool.icon, { + style: { + ...existingStyle, + fontSize: layout === 'compact' ? '1.75rem' : '2rem', + }, + }); + } + + return tool.icon; + }, [layout, tool.icon]); + + const handleSelect = () => { + if (isUnavailable) return; + if (tool.link) { + window.open(tool.link, '_blank', 'noopener,noreferrer'); + return; + } + onSelect(id); + }; + + const handleButtonClick = (event: React.MouseEvent) => { + handleUnlessSpecialClick(event, handleSelect); + }; + + const matchedLine = matchedSynonym + ? t('toolPanel.overlay.matchedSynonym', 'Matches "{{text}}"', { text: matchedSynonym }) + : null; + + const content = ( +
+
+ {iconNode} +
+
+ + {tool.name} + + {layout === 'detailed' && ( + + {tool.description} + + )} + {matchedLine && ( + + {matchedLine} + + )} + {layout === 'detailed' && binding && ( +
+ {t('settings.hotkeys.shortcut', 'Shortcut')} + +
+ )} +
+
+ ); + + const wrappedContent = layout === 'compact' && tooltipContent ? ( + + {content} + + ) : ( + content + ); + + if (navProps) { + return ( + + {wrappedContent} + + ); + } + + if (tool.link && !isUnavailable) { + return ( + + {wrappedContent} + + ); + } + + return ( + + ); +}; + +export default ToolPanelOverlayTile;