From c200e2a1896104e333388747c04d5071c477623b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 4 Oct 2025 21:26:36 +0100 Subject: [PATCH] feat: integrate fullscreen catalog into tool panel --- frontend/src/components/tools/ToolPanel.css | 12 +- frontend/src/components/tools/ToolPanel.tsx | 148 +++++----- .../src/components/tools/ToolPanelOverlay.css | 154 +++------- .../src/components/tools/ToolPanelOverlay.tsx | 265 ++++++++---------- frontend/src/pages/HomePage.css | 51 ++++ frontend/src/pages/HomePage.tsx | 25 +- 6 files changed, 318 insertions(+), 337 deletions(-) diff --git a/frontend/src/components/tools/ToolPanel.css b/frontend/src/components/tools/ToolPanel.css index bed84fd0b..70acb7789 100644 --- a/frontend/src/components/tools/ToolPanel.css +++ b/frontend/src/components/tools/ToolPanel.css @@ -1,3 +1,11 @@ +.tool-panel { + transition: width 0.35s ease, flex 0.35s ease; +} + +.tool-panel--catalog { + background: var(--bg-background); +} + .tool-panel__search-row { display: flex; align-items: center; @@ -17,10 +25,6 @@ transform: scale(1.05); } -.tool-panel--overlay-hidden { - visibility: hidden; -} - .tool-panel__overlay-hint { padding: 0.5rem 1rem; border-bottom: 1px solid var(--border-subtle); diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index ddd5b9819..edbf4654d 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -4,6 +4,7 @@ import ToolPicker from './ToolPicker'; import SearchResults from './SearchResults'; import ToolRenderer from './ToolRenderer'; import ToolSearch from './toolPicker/ToolSearch'; +import ToolPanelOverlay from './ToolPanelOverlay'; import { useSidebarContext } from "../../contexts/SidebarContext"; import rainbowStyles from '../../styles/rainbow.module.css'; import { ActionIcon, Group, ScrollArea, Text, Tooltip } from '@mantine/core'; @@ -38,7 +39,7 @@ export default function ToolPanel() { } = useToolWorkflow(); const isFullscreenMode = toolPanelMode === 'fullscreen'; - const overlayActive = isFullscreenMode && leftPanelView === 'toolPicker' && !isMobile; + const isCatalogActive = isFullscreenMode && leftPanelView === 'toolPicker' && !isMobile; const toggleLabel = isFullscreenMode ? t('toolPanel.modeToggle.sidebar', 'Switch to advanced sidebar') @@ -48,33 +49,42 @@ export default function ToolPanel() { setToolPanelMode(isFullscreenMode ? 'sidebar' : 'fullscreen'); }; + const computedWidth = () => { + if (isMobile) { + return '100%'; + } + + if (isFullscreenMode) { + if (isCatalogActive) { + return '100%'; + } + + if (leftPanelView === 'toolContent' && isPanelVisible) { + return '20rem'; + } + + return isPanelVisible ? '20rem' : '0'; + } + + return isPanelVisible ? '18.5rem' : '0'; + }; + return (
{/* Search Bar - Always visible at the top */} -
- - {!isMobile && ( - - - {isFullscreenMode ? ( - - ) : ( - - )} - - - )} -
+ {!isCatalogActive && ( +
+ + {!isMobile && ( + + + {isFullscreenMode ? ( + + ) : ( + + )} + + + )} +
+ )} - {searchQuery.trim().length > 0 ? ( + {searchQuery.trim().length > 0 && !isCatalogActive ? ( // Searching view (replaces both picker and content)
- {isFullscreenMode && !isMobile ? ( -
- - - {t('toolPanel.overlayHint', 'Select a tool to open it in the workspace.')} - - -
- ) : null} - handleToolSelect(id as ToolId)} - filteredTools={filteredTools} - isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)} - /> + {isCatalogActive ? ( + + ) : ( + <> + {isFullscreenMode && !isMobile ? ( +
+ + + {t('toolPanel.overlayHint', 'Select a tool to open it in the workspace.')} + + +
+ ) : null} + handleToolSelect(id as ToolId)} + filteredTools={filteredTools} + isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)} + /> + + )}
) : ( // Selected Tool Content View diff --git a/frontend/src/components/tools/ToolPanelOverlay.css b/frontend/src/components/tools/ToolPanelOverlay.css index ecf02eb01..032743d5d 100644 --- a/frontend/src/components/tools/ToolPanelOverlay.css +++ b/frontend/src/components/tools/ToolPanelOverlay.css @@ -1,61 +1,39 @@ .tool-panel-overlay { - position: fixed; - inset: 0; - z-index: 1200; - display: flex; - justify-content: center; - align-items: stretch; - background: rgba(10, 10, 10, 0.45); - backdrop-filter: blur(4px); - transition: opacity 0.32s ease, transform 0.32s ease; - opacity: 1; -} - -.tool-panel-overlay--open { - transform: translateX(0); -} - -.tool-panel-overlay--closing { - opacity: 0; - transform: translateX(-6%); - pointer-events: none; -} - -.tool-panel-overlay__paper { - flex: 1; - max-width: 96rem; - margin: 2rem; display: flex; flex-direction: column; + height: 100%; background: var(--bg-background); - border-radius: 1.5rem; - overflow: hidden; - box-shadow: var(--shadow-overlay, 0 24px 64px rgba(15, 23, 42, 0.32)); + color: var(--text-primary); } .tool-panel-overlay__header { display: flex; justify-content: space-between; align-items: center; - padding: 1.75rem; - gap: 1rem; + padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-subtle); - background: var(--bg-elevated); + background: var(--bg-toolbar); +} + +.tool-panel-overlay__heading { + display: flex; + flex-direction: column; + gap: 0.25rem; } .tool-panel-overlay__search { display: flex; - gap: 1.25rem; + flex-wrap: wrap; align-items: center; justify-content: space-between; - padding: 1.25rem 1.75rem; + gap: 0.75rem; + padding: 0.75rem 1.5rem; border-bottom: 1px solid var(--border-subtle); background: var(--bg-toolbar); - flex-wrap: wrap; } .tool-panel-overlay__search-input { - flex: 1 1 22rem; + flex: 1 1 20rem; min-width: 16rem; } @@ -66,15 +44,10 @@ .tool-panel-overlay__search-controls { display: flex; align-items: center; - gap: 1rem; + gap: 0.75rem; flex-wrap: wrap; } -.tool-panel-overlay__layout-toggle { - display: flex; - align-items: center; -} - .tool-panel-overlay__layout-toggle .mantine-SegmentedControl-control { font-weight: 500; } @@ -82,7 +55,7 @@ .tool-panel-overlay__body { flex: 1; min-height: 0; - position: relative; + background: var(--bg-background); } .tool-panel-overlay__scroll { @@ -90,30 +63,28 @@ } .tool-panel-overlay__content { - padding: 2rem; + padding: 1.5rem; display: flex; flex-direction: column; - gap: 2.5rem; + gap: 2rem; } .tool-panel-overlay__section { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 1rem; } .tool-panel-overlay__section-header { display: flex; align-items: baseline; gap: 0.5rem; - color: var(--text-strong); - text-transform: uppercase; - letter-spacing: 0.08em; + color: var(--tool-subcategory-text-color); } .tool-panel-overlay__grid { display: grid; - gap: 1.25rem; + gap: 1rem; } .tool-panel-overlay__grid--compact { @@ -142,31 +113,31 @@ .tool-panel-overlay__tile { width: 100%; height: 100%; - background: var(--bg-elevated); + background: var(--tool-card-background, var(--bg-elevated)); border: 1px solid var(--border-subtle); - border-radius: 1.25rem; - padding: 1.25rem; + border-radius: 1rem; + padding: 1rem; display: flex; - gap: 1rem; + gap: 0.75rem; color: var(--text-primary); - transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; } .tool-panel-overlay__tile[data-variant="compact"] { flex-direction: column; align-items: flex-start; - min-height: 10.5rem; + min-height: 10rem; } .tool-panel-overlay__tile[data-variant="detailed"] { flex-direction: row; align-items: flex-start; - min-height: 8.75rem; + min-height: 9rem; } .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); + border-color: var(--accent-primary, var(--primary-border, var(--mantine-color-pink-6))); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 20%, transparent); } .tool-panel-overlay__tile[data-disabled="true"] { @@ -174,9 +145,9 @@ } .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); + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 30%, var(--border-subtle)); + box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12); } .tool-panel-overlay__tile-icon { @@ -185,84 +156,49 @@ 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); + border-radius: 0.75rem; + background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 10%, 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; + font-size: 1.8rem; } .tool-panel-overlay__tile-body { display: flex; flex-direction: column; - gap: 0.35rem; + gap: 0.4rem; } .tool-panel-overlay__tile-name { - font-size: 1rem; - line-height: 1.35; + color: var(--text-primary); } .tool-panel-overlay__tile-description { - line-height: 1.45; + line-height: 1.4; } .tool-panel-overlay__tile-match { - font-size: 0.75rem; - letter-spacing: 0.02em; - text-transform: uppercase; + font-style: italic; } .tool-panel-overlay__tile-hotkey { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.35rem; font-size: 0.75rem; - color: var(--mantine-color-dimmed); + color: var(--text-muted); } -.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; - } - +@media (max-width: 1280px) { .tool-panel-overlay__content { - padding: 1.5rem; - } -} - -@media (max-width: 1024px) { - .tool-panel-overlay__paper { - 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)); + .tool-panel-overlay__search { + padding-inline: 1.25rem; } } diff --git a/frontend/src/components/tools/ToolPanelOverlay.tsx b/frontend/src/components/tools/ToolPanelOverlay.tsx index 836680d4b..d36cbd0ff 100644 --- a/frontend/src/components/tools/ToolPanelOverlay.tsx +++ b/frontend/src/components/tools/ToolPanelOverlay.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { ActionIcon, Badge, Group, Paper, ScrollArea, SegmentedControl, Text, Tooltip } from '@mantine/core'; +import { ActionIcon, Badge, Group, 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'; @@ -15,22 +15,17 @@ 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'; + return 'detailed'; } const stored = window.localStorage.getItem(LAYOUT_STORAGE_KEY); - return stored === 'detailed' ? 'detailed' : 'compact'; + return stored === 'compact' ? 'compact' : 'detailed'; }; -export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { +export default function ToolPanelOverlay() { const { t } = useTranslation(); const { searchQuery, @@ -44,8 +39,6 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { setLeftPanelView, } = useToolWorkflow(); - const [shouldRender, setShouldRender] = useState(isOpen); - const [isClosing, setIsClosing] = useState(false); const [layout, setLayout] = useState(getInitialLayout); const { sections, searchGroups } = useToolSections(filteredTools, searchQuery); @@ -55,38 +48,6 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { window.localStorage.setItem(LAYOUT_STORAGE_KEY, layout); }, [layout]); - useEffect(() => { - if (isOpen) { - setShouldRender(true); - setIsClosing(false); - document.documentElement.style.setProperty('overflow', 'hidden'); - return; - } - - if (shouldRender) { - setIsClosing(true); - const timeout = window.setTimeout(() => { - setShouldRender(false); - setIsClosing(false); - document.documentElement.style.removeProperty('overflow'); - }, EXIT_ANIMATION_MS); - return () => { - window.clearTimeout(timeout); - document.documentElement.style.removeProperty('overflow'); - }; - } - - document.documentElement.style.removeProperty('overflow'); - setShouldRender(false); - }, [isOpen, shouldRender]); - - useEffect(() => { - if (!isOpen) return; - return () => { - document.documentElement.style.removeProperty('overflow'); - }; - }, [isOpen]); - const showSearchResults = useMemo(() => searchQuery.trim().length > 0, [searchQuery]); const totalToolCount = showSearchResults ? filteredTools.length : Object.keys(toolRegistry).length; @@ -108,13 +69,9 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { return allSection ? allSection.subcategories : []; }, [searchGroups, sections, showSearchResults]); - if (!shouldRender) { - return null; - } - const handleClose = () => { setSearchQuery(''); - setLeftPanelView('hidden'); + setLeftPanelView(selectedToolKey ? 'toolContent' : 'hidden'); }; const toggleLabel = toolPanelMode === 'fullscreen' @@ -125,118 +82,126 @@ export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { return (
- -
-
- - {t('toolPanel.overlay.title', 'All tools')} - - - {t('toolPanel.overlay.subtitle', 'Browse every tool in the legacy fullscreen catalog.')} - -
- - - setToolPanelMode(toolPanelMode === 'fullscreen' ? 'sidebar' : 'fullscreen')} - aria-label={toggleLabel} - > - {toolPanelMode === 'fullscreen' ? ( - - ) : ( - - )} - - - - - - - - -
+
+
+ + {t('toolPanel.overlay.title', 'All tools')} + + + {t('toolPanel.overlay.subtitle', 'Browse every tool in the legacy fullscreen catalog.')} + +
+ + + setToolPanelMode(toolPanelMode === 'fullscreen' ? 'sidebar' : 'fullscreen')} + aria-label={toggleLabel} + > + {toolPanelMode === 'fullscreen' ? ( + + ) : ( + + )} + + + + + + + + +
-
-
- +
+ +
+
+
+ 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' }, + ]} />
-
-
- 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: totalToolCount, - defaultValue: '{{count}} tools available', - })} - -
+ + {t('toolPanel.overlay.totalLabel', { + count: totalToolCount, + defaultValue: '{{count}} tools available', + })} +
+
-
- -
- {subcategoryGroups.length === 0 ? ( -
- -
- ) : ( - subcategoryGroups.map(group => ( -
-
- - {getSubcategoryLabel(t, group.subcategoryId)} - -
-
- {group.tools.map(({ id, tool }) => ( +
+ +
+ {subcategoryGroups.length === 0 ? ( + + ) : ( + subcategoryGroups.map(({ key, tools }) => ( +
+
+ + {getSubcategoryLabel(key, t)} + + + {tools.length} + +
+
+ {tools.map(tool => { + const [toolId, toolConfig] = tool.item; + const matchedText = matchedTextMap.get(toolId); + return ( handleToolSelect(toolId as ToolId)} - isSelected={selectedToolKey === id} - matchedSynonym={matchedTextMap.get(id)} + onSelect={(nextId) => handleToolSelect(nextId as ToolId)} + isSelected={selectedToolKey === toolId} + matchedSynonym={matchedText} /> - ))} -
-
- )) - )} -
-
-
- + ); + })} +
+
+ )) + )} +
+
+
); } diff --git a/frontend/src/pages/HomePage.css b/frontend/src/pages/HomePage.css index 219bab0f1..ff49d34d2 100644 --- a/frontend/src/pages/HomePage.css +++ b/frontend/src/pages/HomePage.css @@ -166,3 +166,54 @@ font-weight: 500; color: var(--text-muted); } + +.home-desktop-layout { + width: 100%; + transition: background-color 0.3s ease; +} + +.home-desktop-layout__quick, +.home-desktop-layout__tool-panel, +.home-desktop-layout__workbench, +.home-desktop-layout__right-rail { + display: flex; + overflow: hidden; + transition: max-width 0.35s ease, transform 0.35s ease, opacity 0.3s ease; +} + +.home-desktop-layout__quick { + max-width: 5rem; +} + +.home-desktop-layout__tool-panel { + flex: 0 0 auto; + min-width: 0; +} + +.home-desktop-layout__workbench { + flex: 1 1 auto; + min-width: 0; +} + +.home-desktop-layout__right-rail { + flex: 0 0 auto; +} + +.home-desktop-layout--catalog .home-desktop-layout__quick { + max-width: 0; + transform: translateX(-100%); + opacity: 0; + pointer-events: none; +} + +.home-desktop-layout--catalog .home-desktop-layout__tool-panel { + flex: 1 1 auto; +} + +.home-desktop-layout--catalog .home-desktop-layout__workbench, +.home-desktop-layout--catalog .home-desktop-layout__right-rail { + max-width: 0; + opacity: 0; + transform: translateX(2rem); + pointer-events: none; +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index c6b3fb8dc..379d4c31f 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -9,7 +9,6 @@ import { useMediaQuery } from "@mantine/hooks"; import AppsIcon from '@mui/icons-material/AppsRounded'; import ToolPanel from "../components/tools/ToolPanel"; -import ToolPanelOverlay from "../components/tools/ToolPanelOverlay"; import ToolPanelModePrompt from "../components/tools/ToolPanelModePrompt"; import Workbench from "../components/layout/Workbench"; import QuickAccessBar from "../components/shared/QuickAccessBar"; @@ -133,12 +132,11 @@ export default function HomePage() { // Note: File selection limits are now handled directly by individual tools - const showFullscreenOverlay = !isMobile && toolPanelMode === 'fullscreen' && leftPanelView === 'toolPicker'; + const desktopCatalogActive = !isMobile && toolPanelMode === 'fullscreen' && leftPanelView === 'toolPicker'; return (
- {isMobile ? (
@@ -242,13 +240,22 @@ export default function HomePage() { align="flex-start" gap={0} h="100%" - className="flex-nowrap flex" + className={`flex-nowrap flex home-desktop-layout ${desktopCatalogActive ? 'home-desktop-layout--catalog' : ''}`} > - - - - +
+ +
+
+ +
+
+ +
+
+ +
)}