diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 405643877..1d19d5465 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -1946,6 +1946,25 @@ viewer.zoomIn=Zoom in # Tool Picker toolPicker.searchPlaceholder=Search tools... toolPicker.noToolsFound=No tools found +toolPanel.toggle.legacy=Switch to legacy mode +toolPanel.toggle.sidebar=Switch to sidebar mode +toolPanel.placeholder=Choose a tool to get started +toolPanel.legacy.heading=All tools (legacy view) +toolPanel.legacy.tagline=Browse and launch tools while keeping the classic full-width gallery. +toolPanel.legacy.descriptionsOn=Showing descriptions +toolPanel.legacy.descriptionsOff=Descriptions hidden +toolPanel.legacy.noResults=Try adjusting your search or toggle descriptions to find what you need. +toolPanel.legacy.matchedSynonym=Matches "{{text}}" +toolPanel.modePrompt.title=Choose how you browse tools +toolPanel.modePrompt.description=Preview both layouts and decide how you want to explore Stirling PDF tools. +toolPanel.modePrompt.sidebarTitle=Advanced sidebar +toolPanel.modePrompt.sidebarDescription=Keep tools alongside your workspace for quick switching. +toolPanel.modePrompt.recommended=Recommended +toolPanel.modePrompt.chooseSidebar=Use advanced sidebar +toolPanel.modePrompt.legacyTitle=Legacy fullscreen +toolPanel.modePrompt.legacyDescription=Browse every tool in a catalogue that covers the workspace until you pick one. +toolPanel.modePrompt.chooseLegacy=Use legacy fullscreen +toolPanel.modePrompt.dismiss=Maybe later pageEditor.reset=Reset Changes pageEditor.zoomIn=Zoom In pageEditor.zoomOut=Zoom Out diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index 2b4569dc5..fc31a851c 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -176,7 +176,7 @@ export default function RightRail() { }, [currentView]); return ( -
+
{topButtons.length > 0 && ( <> @@ -481,4 +481,3 @@ export default function RightRail() { ); } - diff --git a/frontend/src/components/tools/LegacyToolList.tsx b/frontend/src/components/tools/LegacyToolList.tsx new file mode 100644 index 000000000..c4e2c16da --- /dev/null +++ b/frontend/src/components/tools/LegacyToolList.tsx @@ -0,0 +1,214 @@ +import React, { useMemo } from 'react'; +import { Badge, Text } from '@mantine/core'; +import { Tooltip } from '../shared/Tooltip'; +import { useTranslation } from 'react-i18next'; +import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; +import { ToolId } from '../../types/toolId'; +import { useToolSections } from '../../hooks/useToolSections'; +import { getSubcategoryLabel } from '../../data/toolsTaxonomy'; +import NoToolsFound from './shared/NoToolsFound'; +import './ToolPanel.css'; + +interface LegacyToolListProps { + filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + searchQuery: string; + showDescriptions: boolean; + selectedToolKey: string | null; + matchedTextMap: Map; + onSelect: (id: ToolId) => void; +} + +const LegacyToolList = ({ + filteredTools, + searchQuery, + showDescriptions, + selectedToolKey, + matchedTextMap, + onSelect, +}: LegacyToolListProps) => { + const { t } = useTranslation(); + + const { sections, searchGroups } = useToolSections(filteredTools, searchQuery); + + const tooltipPortalTarget = typeof document !== 'undefined' ? document.body : undefined; + + const subcategoryGroups = useMemo(() => { + if (searchQuery.trim().length > 0) { + return searchGroups; + } + const allSection = sections.find(section => section.key === 'all'); + return allSection ? allSection.subcategories : []; + }, [searchGroups, sections, searchQuery]); + + if (subcategoryGroups.length === 0) { + return ( +
+ + + {t('toolPanel.legacy.noResults', 'Try adjusting your search or toggle descriptions to find what you need.')} + +
+ ); + } + + const containerClass = showDescriptions + ? 'tool-panel__legacy-groups tool-panel__legacy-groups--detailed' + : 'tool-panel__legacy-groups tool-panel__legacy-groups--compact'; + + return ( +
+ {subcategoryGroups.map(({ subcategoryId, tools }) => ( +
+
+ + {getSubcategoryLabel(t, subcategoryId)} + + + {tools.length} + +
+ + {showDescriptions ? ( +
+ {tools.map(({ id, tool }) => { + const matchedText = matchedTextMap.get(id); + const isSelected = selectedToolKey === id; + const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; + + let iconNode: React.ReactNode = null; + if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) { + const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>; + iconNode = React.cloneElement(element, { + style: { + ...(element.props.style || {}), + fontSize: '1.75rem', + }, + }); + } else { + iconNode = tool.icon; + } + + const handleClick = () => { + if (isDisabled) return; + if (tool.link) { + window.open(tool.link, '_blank', 'noopener,noreferrer'); + return; + } + onSelect(id as ToolId); + }; + + return ( + + ); + })} +
+ ) : ( +
+ {tools.map(({ id, tool }) => { + const matchedText = matchedTextMap.get(id); + const isSelected = selectedToolKey === id; + const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; + + let iconNode: React.ReactNode = null; + if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) { + const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>; + iconNode = React.cloneElement(element, { + style: { + ...(element.props.style || {}), + fontSize: '1.5rem', + }, + }); + } else { + iconNode = tool.icon; + } + + const handleClick = () => { + if (isDisabled) return; + if (tool.link) { + window.open(tool.link, '_blank', 'noopener,noreferrer'); + return; + } + onSelect(id as ToolId); + }; + + const baseButton = ( + + ); + + if (showDescriptions || !tool.description) { + return React.cloneElement(baseButton, { key: id }); + } + + return ( + + {baseButton} + + ); + })} +
+ )} +
+ ))} +
+ ); +}; + +export default LegacyToolList; diff --git a/frontend/src/components/tools/LegacyToolSurface.tsx b/frontend/src/components/tools/LegacyToolSurface.tsx new file mode 100644 index 000000000..995879205 --- /dev/null +++ b/frontend/src/components/tools/LegacyToolSurface.tsx @@ -0,0 +1,131 @@ +import { useEffect } from 'react'; +import { ActionIcon, Group, ScrollArea, Switch, Text, Tooltip } from '@mantine/core'; +import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded'; +import { useTranslation } from 'react-i18next'; +import ToolSearch from './toolPicker/ToolSearch'; +import LegacyToolList from './LegacyToolList'; +import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; +import { ToolId } from '../../types/toolId'; +import './ToolPanel.css'; + +interface LegacyToolSurfaceProps { + searchQuery: string; + toolRegistry: Record; + filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + selectedToolKey: string | null; + showDescriptions: boolean; + matchedTextMap: Map; + onSearchChange: (value: string) => void; + onSelect: (id: ToolId) => void; + onToggleDescriptions: () => void; + onExitLegacyMode: () => void; + toggleLabel: string; + geometry: { + left: number; + top: number; + width: number; + height: number; + } | null; +} + +const LegacyToolSurface = ({ + searchQuery, + toolRegistry, + filteredTools, + selectedToolKey, + showDescriptions, + matchedTextMap, + onSearchChange, + onSelect, + onToggleDescriptions, + onExitLegacyMode, + toggleLabel, + geometry, +}: LegacyToolSurfaceProps) => { + const { t } = useTranslation(); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onExitLegacyMode(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onExitLegacyMode]); + + const style = geometry + ? { + left: `${geometry.left}px`, + top: `${geometry.top}px`, + width: `${geometry.width}px`, + height: `${geometry.height}px`, + } + : undefined; + + return ( +
+
+
+
+ + {t('toolPanel.legacy.heading', 'All tools (legacy view)')} + + + {t('toolPanel.legacy.tagline', 'Browse and launch tools while keeping the classic full-width gallery.')} + +
+ + + + + +
+ +
+ + onToggleDescriptions()} + size="md" + labelPosition="left" + label={showDescriptions ? t('toolPanel.legacy.descriptionsOn', 'Showing descriptions') : t('toolPanel.legacy.descriptionsOff', 'Descriptions hidden')} + /> +
+ +
+ + + +
+
+
+ ); +}; + +export default LegacyToolSurface; diff --git a/frontend/src/components/tools/ToolPanel.css b/frontend/src/components/tools/ToolPanel.css new file mode 100644 index 000000000..27eabf7c7 --- /dev/null +++ b/frontend/src/components/tools/ToolPanel.css @@ -0,0 +1,351 @@ +.tool-panel { + position: relative; + transition: width 0.3s ease, max-width 0.3s ease; +} + +.tool-panel--legacy-active { + overflow: visible !important; +} + +.tool-panel__search-row { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; +} + +.tool-panel__search-row .search-input-container { + flex: 1 1 auto; +} + +.tool-panel__mode-toggle { + transition: transform 0.2s ease; +} + +.tool-panel__mode-toggle:hover { + transform: scale(1.04); +} + +.tool-panel--legacy { + background: var(--bg-toolbar); +} + +.tool-panel__placeholder { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 0.9rem; + padding: 1.5rem; + text-align: center; +} + +.tool-panel__legacy-surface { + position: fixed; + display: flex; + pointer-events: none; + z-index: 1200; + top: 0; + left: 0; + width: 0; + height: 0; +} + +.tool-panel__legacy-surface-inner { + pointer-events: auto; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + border-radius: 0 1.25rem 1.25rem 0; + background: + linear-gradient( + 140deg, + color-mix(in srgb, var(--bg-toolbar) 96%, transparent), + color-mix(in srgb, var(--bg-background) 90%, transparent) + ) + padding-box; + border: 1px solid color-mix(in srgb, var(--border-subtle) 75%, transparent); + box-shadow: + 0 24px 64px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 25%, transparent), + 0 6px 18px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 30%, transparent); + backdrop-filter: blur(18px); + overflow: hidden; + animation: tool-panel-legacy-slide 0.28s ease forwards; +} + +.tool-panel__legacy-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + padding: 1.25rem 1.75rem; + border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-toolbar) 86%, transparent), + transparent 85% + ); +} + +.tool-panel__legacy-heading { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.tool-panel__legacy-controls { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 1.75rem; + border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-toolbar) 84%, transparent), + color-mix(in srgb, var(--bg-background) 72%, transparent) + ); +} + +.tool-panel__legacy-controls .search-input-container { + flex: 1 1 auto; +} + +.tool-panel__legacy-body { + flex: 1; + min-height: 0; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-background) 86%, transparent), + color-mix(in srgb, var(--bg-toolbar) 78%, transparent) + ); +} + +.tool-panel__legacy-scroll { + height: 100%; +} + +/* legacy group layout */ +.tool-panel__legacy-groups { + padding: 1.5rem 1.75rem; + column-width: 18rem; + column-gap: 1.5rem; +} + +.tool-panel__legacy-groups--compact { + column-width: 17rem; +} + +.tool-panel__legacy-groups--detailed { + column-width: auto; +} + +.tool-panel__legacy-group { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; + margin: 0 0 1.5rem; + padding: 0.65rem 0.75rem 1rem; + border-radius: 1rem; + background: color-mix(in srgb, var(--bg-toolbar) 82%, transparent); + border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent); + box-shadow: 0 14px 32px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.45)) 18%, transparent); + break-inside: avoid; + backdrop-filter: blur(10px); +} + +.tool-panel__legacy-section-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.1rem 0.15rem 0.35rem; +} + +.tool-panel__legacy-grid { + display: grid; + gap: 0.75rem; +} + +.tool-panel__legacy-grid--detailed { + grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); +} + +.tool-panel__legacy-item { + all: unset; + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: flex-start; + padding: 0.85rem 0.95rem; + border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + border-radius: 0.95rem; + background: color-mix(in srgb, var(--bg-toolbar) 88%, transparent); + backdrop-filter: blur(6px); + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; + width: 100%; + box-sizing: border-box; +} + +.tool-panel__legacy-item:focus-visible { + outline: 2px solid var(--accent-primary, var(--mantine-color-pink-6)); + outline-offset: 3px; +} + +.tool-panel__legacy-item[aria-disabled="true"], +.tool-panel__legacy-item:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.tool-panel__legacy-item:hover:not([aria-disabled="true"]):not(:disabled) { + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 38%, var(--border-subtle)); + box-shadow: var(--shadow-xl, 0 18px 34px rgba(15, 23, 42, 0.14)); +} + +.tool-panel__legacy-item--selected { + border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 52%, var(--border-subtle)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 28%, transparent); +} + +.tool-panel__legacy-item--detailed { + min-height: 7.5rem; +} + +.tool-panel__legacy-icon { + display: flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + border-radius: 0.75rem; + background: color-mix(in srgb, var(--bg-muted) 75%, transparent); + color: color-mix(in srgb, var(--text-primary) 90%, var(--text-muted)); + flex-shrink: 0; +} + +.tool-panel__legacy-icon svg { + font-size: 1.65rem; +} + +.tool-panel__legacy-body { + display: flex; + flex-direction: column; + gap: 0.4rem; + text-align: left; +} + +.tool-panel__legacy-name { + color: var(--text-primary); +} + +.tool-panel__legacy-description { + line-height: 1.45; +} + +.tool-panel__legacy-match { + font-style: italic; +} + +.tool-panel__legacy-list { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.tool-panel__legacy-list-item { + all: unset; + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.55rem 0.5rem 0.55rem 0.65rem; + border-radius: 0.65rem; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease; + background: color-mix(in srgb, var(--bg-toolbar) 86%, transparent); + border: 1px solid transparent; + width: 100%; + box-sizing: border-box; +} + +.tool-panel__legacy-list-item[aria-disabled="true"], +.tool-panel__legacy-list-item:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.tool-panel__legacy-list-item:hover:not([aria-disabled="true"]):not(:disabled), +.tool-panel__legacy-list-item--selected { + background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 22%, var(--bg-toolbar)); + border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 30%, var(--border-subtle)); +} + +.tool-panel__legacy-list-item--selected { + transform: translateX(2px); +} + +.tool-panel__legacy-list-icon { + display: flex; + align-items: center; + justify-content: center; + width: 2.1rem; + height: 2.1rem; + border-radius: 0.6rem; + background: color-mix(in srgb, var(--bg-muted) 70%, transparent); + color: color-mix(in srgb, var(--text-primary) 88%, var(--text-muted)); + flex-shrink: 0; +} + +.tool-panel__legacy-list-body { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.2rem; + text-align: left; +} + +.tool-panel__legacy-empty { + padding: 2rem 1.75rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +@keyframes tool-panel-legacy-slide { + from { + transform: translateX(-6%) scaleX(0.85); + opacity: 0; + } + to { + transform: translateX(0) scaleX(1); + opacity: 1; + } +} + +@media (max-width: 1440px) { + .tool-panel__legacy-content { + padding-inline: 1.5rem; + } + + .tool-panel__legacy-grid--compact { + column-width: 15rem; + } +} + +@media (max-width: 1280px) { + .tool-panel__legacy-controls { + flex-direction: column; + align-items: stretch; + } + + .tool-panel__legacy-controls .mantine-Switch-root { + justify-content: flex-end; + } + + .tool-panel__legacy-grid--compact { + column-width: 14rem; + } +} diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index beefc2c45..4bc583db3 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -1,3 +1,4 @@ +import { useEffect, useMemo, useState, useLayoutEffect } from 'react'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import ToolPicker from './ToolPicker'; @@ -6,20 +7,24 @@ import ToolRenderer from './ToolRenderer'; import ToolSearch from './toolPicker/ToolSearch'; import { useSidebarContext } from "../../contexts/SidebarContext"; import rainbowStyles from '../../styles/rainbow.module.css'; -import { ScrollArea } from '@mantine/core'; +import { ActionIcon, ScrollArea, Tooltip } from '@mantine/core'; import { ToolId } from '../../types/toolId'; import { useMediaQuery } from '@mantine/hooks'; +import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded'; +import DashboardCustomizeRoundedIcon from '@mui/icons-material/DashboardCustomizeRounded'; +import { useTranslation } from 'react-i18next'; +import LegacyToolSurface from './LegacyToolSurface'; +import './ToolPanel.css'; // No props needed - component uses context export default function ToolPanel() { + const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); const { sidebarRefs } = useSidebarContext(); - const { toolPanelRef } = sidebarRefs; + const { toolPanelRef, quickAccessRef } = sidebarRefs; const isMobile = useMediaQuery('(max-width: 1024px)'); - - // Use context-based hooks to eliminate prop drilling const { leftPanelView, isPanelVisible, @@ -27,84 +32,235 @@ export default function ToolPanel() { filteredTools, toolRegistry, setSearchQuery, + selectedToolKey, + handleToolSelect, + setPreviewFile, + toolPanelMode, + setToolPanelMode, + setLeftPanelView, } = useToolWorkflow(); - const { selectedToolKey, handleToolSelect } = useToolWorkflow(); - const { setPreviewFile } = useToolWorkflow(); + const isLegacyMode = toolPanelMode === 'legacy'; + const legacyExpanded = isLegacyMode && leftPanelView === 'toolPicker' && !isMobile; + const [legacyGeometry, setLegacyGeometry] = useState<{ left: number; top: number; width: number; height: number } | null>(null); + + const LEGACY_DESCRIPTION_STORAGE_KEY = 'legacyToolDescriptions'; + const [showLegacyDescriptions, setShowLegacyDescriptions] = useState(() => { + if (typeof window === 'undefined') { + return true; + } + + const stored = window.localStorage.getItem(LEGACY_DESCRIPTION_STORAGE_KEY); + if (stored === null) { + return true; + } + return stored === 'true'; + }); + + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + window.localStorage.setItem(LEGACY_DESCRIPTION_STORAGE_KEY, String(showLegacyDescriptions)); + }, [showLegacyDescriptions]); + + useLayoutEffect(() => { + if (!legacyExpanded) { + setLegacyGeometry(null); + return; + } + + const panelEl = toolPanelRef.current; + if (!panelEl) { + setLegacyGeometry(null); + return; + } + + const rightRailEl = () => document.querySelector('[data-sidebar="right-rail"]') as HTMLElement | null; + + const updateGeometry = () => { + const rect = panelEl.getBoundingClientRect(); + const rail = rightRailEl(); + const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; + const width = Math.max(360, window.innerWidth - rect.left - rightOffset); + const height = Math.max(rect.height, window.innerHeight - rect.top); + setLegacyGeometry({ + left: rect.left, + top: rect.top, + width, + height, + }); + }; + + updateGeometry(); + + const handleResize = () => updateGeometry(); + window.addEventListener('resize', handleResize); + + let resizeObserver: ResizeObserver | null = null; + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(() => updateGeometry()); + resizeObserver.observe(panelEl); + if (quickAccessRef.current) { + resizeObserver.observe(quickAccessRef.current); + } + const rail = rightRailEl(); + if (rail) { + resizeObserver.observe(rail); + } + } + + return () => { + window.removeEventListener('resize', handleResize); + resizeObserver?.disconnect(); + }; + }, [legacyExpanded, quickAccessRef, toolPanelRef]); + + const toggleLabel = isLegacyMode + ? t('toolPanel.toggle.sidebar', 'Switch to sidebar mode') + : t('toolPanel.toggle.legacy', 'Switch to legacy mode'); + + const handleModeToggle = () => { + const nextMode = isLegacyMode ? 'sidebar' : 'legacy'; + setToolPanelMode(nextMode); + + if (nextMode === 'legacy' && leftPanelView !== 'toolPicker') { + setLeftPanelView('toolPicker'); + } + }; + + const computedWidth = () => { + if (isMobile) { + return '100%'; + } + + if (!isPanelVisible) { + return '0'; + } + + return '18.5rem'; + }; + + const matchedTextMap = useMemo(() => { + const map = new Map(); + filteredTools.forEach(({ item: [id], matchedText }) => { + if (matchedText) { + map.set(id, matchedText); + } + }); + return map; + }, [filteredTools]); return (
-
- {/* Search Bar - Always visible at the top */} + {!legacyExpanded && (
- -
+
+ + {!isMobile && ( + + + {isLegacyMode ? ( + + ) : ( + + )} + + + )} +
- {searchQuery.trim().length > 0 ? ( - // Searching view (replaces both picker and content) -
+ {searchQuery.trim().length > 0 ? ( +
handleToolSelect(id as ToolId)} searchQuery={searchQuery} /> -
- ) : leftPanelView === 'toolPicker' ? ( - // Tool Picker View -
- handleToolSelect(id as ToolId)} - filteredTools={filteredTools} - isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)} - /> -
- ) : ( - // Selected Tool Content View -
- {/* Tool content */} -
- - {selectedToolKey && ( - - )} -
-
- )} -
+ ) : leftPanelView === 'toolPicker' ? ( +
+ handleToolSelect(id as ToolId)} + filteredTools={filteredTools} + isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)} + /> +
+ ) : ( +
+
+ + {selectedToolKey ? ( + + ) : ( +
+ {t('toolPanel.placeholder', 'Choose a tool to get started')} +
+ )} +
+
+
+ )} +
+ )} + + {legacyExpanded && ( + handleToolSelect(id as ToolId)} + onToggleDescriptions={() => setShowLegacyDescriptions((prev) => !prev)} + onExitLegacyMode={() => setToolPanelMode('sidebar')} + toggleLabel={toggleLabel} + geometry={legacyGeometry} + /> + )}
); } diff --git a/frontend/src/components/tools/ToolPanelModePrompt.css b/frontend/src/components/tools/ToolPanelModePrompt.css new file mode 100644 index 000000000..0bc20c4de --- /dev/null +++ b/frontend/src/components/tools/ToolPanelModePrompt.css @@ -0,0 +1,167 @@ +.tool-panel-mode-prompt__modal { + background: color-mix(in srgb, var(--bg-toolbar) 94%, transparent); + border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + box-shadow: 0 32px 64px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 20%, transparent); + max-width: min(46rem, 100%); +} + +.tool-panel-mode-prompt__options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; +} + +.tool-panel-mode-prompt__card { + display: flex; + flex-direction: column; + gap: 1rem; + background: linear-gradient(145deg, + color-mix(in srgb, var(--bg-surface) 96%, transparent), + color-mix(in srgb, var(--bg-muted) 70%, transparent) + ); + width: 100%; + max-width: 19rem; +} + +.tool-panel-mode-prompt__card--sidebar { + border: 1px solid color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 18%, var(--border-subtle)); + background: linear-gradient(165deg, + color-mix(in srgb, var(--bg-surface) 96%, transparent), + color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 8%, transparent) + ); +} + +.tool-panel-mode-prompt__preview { + border-radius: 0.9rem; + border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + background: linear-gradient(135deg, color-mix(in srgb, var(--bg-muted) 82%, transparent), transparent 75%); + padding: 0.75rem; + width: 100%; + display: flex; + gap: 0.65rem; + min-height: 6.5rem; + margin-top: auto; +} + +.tool-panel-mode-prompt__preview--sidebar { + align-items: stretch; +} + +.tool-panel-mode-prompt__sidebar-panel { + width: 3rem; + border-radius: 0.65rem; + padding: 0.45rem; + display: flex; + flex-direction: column; + gap: 0.45rem; + background: linear-gradient(180deg, + color-mix(in srgb, var(--bg-muted) 88%, transparent), + color-mix(in srgb, var(--bg-muted) 72%, transparent) + ); + border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent); +} + +.tool-panel-mode-prompt__sidebar-search { + height: 0.5rem; + border-radius: 0.4rem; + background: color-mix(in srgb, var(--bg-background) 90%, transparent); + border: 1px solid color-mix(in srgb, var(--border-subtle) 60%, transparent); +} + +.tool-panel-mode-prompt__sidebar-item { + height: 0.55rem; + border-radius: 0.35rem; + background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 32%, var(--bg-muted)); +} + +.tool-panel-mode-prompt__sidebar-item--muted { + background: color-mix(in srgb, var(--bg-background) 88%, transparent); +} + +.tool-panel-mode-prompt__workspace { + flex: 1; + border-radius: 0.65rem; + border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent); + padding: 0.5rem; + display: grid; + gap: 0.35rem; + grid-template-rows: 1.4fr 0.6fr; + background: linear-gradient(160deg, + color-mix(in srgb, var(--bg-background) 94%, transparent), + color-mix(in srgb, var(--bg-muted) 68%, transparent) + ); +} + +.tool-panel-mode-prompt__workspace-page { + border-radius: 0.45rem; + background: color-mix(in srgb, var(--bg-surface) 96%, transparent); + border: 1px solid color-mix(in srgb, var(--border-subtle) 55%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bg-background) 60%, transparent); +} + +.tool-panel-mode-prompt__workspace-page--secondary { + opacity: 0.55; +} + +.tool-panel-mode-prompt__preview--legacy { + align-items: flex-start; + justify-content: center; + padding: 0.65rem 0.6rem; +} + +.tool-panel-mode-prompt__legacy-columns { + width: 100%; + display: flex; + gap: 0.45rem; + justify-content: space-between; +} + +.tool-panel-mode-prompt__legacy-column { + flex: 1; + display: grid; + gap: 0.3rem; +} + +.tool-panel-mode-prompt__legacy-card { + border-radius: 0.45rem; + border: 1px solid color-mix(in srgb, var(--border-subtle) 55%, transparent); + background: linear-gradient(150deg, + color-mix(in srgb, var(--bg-muted) 88%, transparent), + color-mix(in srgb, var(--bg-background) 76%, transparent) + ); + height: 1.2rem; +} + +.tool-panel-mode-prompt__legacy-card--muted { + opacity: 0.55; +} + +.tool-panel-mode-prompt__card-content { + display: flex; + flex-direction: column; + gap: 1rem; + flex: 1; +} + +.tool-panel-mode-prompt__action { + box-shadow: none; +} + +.tool-panel-mode-prompt__action:hover { + box-shadow: 0 10px 18px color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 25%, transparent); +} + +.tool-panel-mode-prompt__maybe-later { + color: color-mix(in srgb, var(--text-secondary) 90%, var(--text-muted)); +} + +.tool-panel-mode-prompt__maybe-later:hover { + background: color-mix(in srgb, var(--bg-muted) 78%, transparent); +} + +@media (max-width: 600px) { + .tool-panel-mode-prompt__options { + grid-template-columns: 1fr; + justify-items: stretch; + } +} diff --git a/frontend/src/components/tools/ToolPanelModePrompt.tsx b/frontend/src/components/tools/ToolPanelModePrompt.tsx new file mode 100644 index 000000000..eb131c454 --- /dev/null +++ b/frontend/src/components/tools/ToolPanelModePrompt.tsx @@ -0,0 +1,161 @@ +import { useEffect, useState } from 'react'; +import { Badge, Button, Card, Group, Modal, Stack, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useToolWorkflow, TOOL_PANEL_MODE_STORAGE_KEY } from '../../contexts/ToolWorkflowContext'; +import './ToolPanelModePrompt.css'; + +type ToolPanelModeOption = 'sidebar' | 'legacy'; + +const PROMPT_SEEN_KEY = 'toolPanelModePromptSeen'; + +const ToolPanelModePrompt = () => { + const { t } = useTranslation(); + const { toolPanelMode, setToolPanelMode } = useToolWorkflow(); + const [opened, setOpened] = useState(false); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + const hasSeenPrompt = window.localStorage.getItem(PROMPT_SEEN_KEY); + const storedPreference = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY); + + if (!hasSeenPrompt && !storedPreference) { + setOpened(true); + } + + setHydrated(true); + }, []); + + const persistSeen = () => { + if (typeof window === 'undefined') { + return; + } + + window.localStorage.setItem(PROMPT_SEEN_KEY, 'true'); + }; + + const handleSelect = (mode: ToolPanelModeOption) => { + setToolPanelMode(mode); + persistSeen(); + setOpened(false); + }; + + const handleDismiss = () => { + persistSeen(); + setOpened(false); + }; + + if (!hydrated) { + return null; + } + + return ( + + + + {t('toolPanel.modePrompt.description', 'Preview both layouts and decide how you want to explore Stirling PDF tools.')} + +
+ + + + + {t('toolPanel.modePrompt.sidebarTitle', 'Advanced sidebar')} + + {t('toolPanel.modePrompt.sidebarDescription', 'Keep tools alongside your workspace for quick switching.')} + + + + {t('toolPanel.modePrompt.recommended', 'Recommended')} + + +
+
+ + + + + +
+
+
+
+
+
+ + + + + + + {t('toolPanel.modePrompt.legacyTitle', 'Legacy fullscreen')} + + {t('toolPanel.modePrompt.legacyDescription', 'Browse every tool in a catalogue that covers the workspace until you pick one.')} + + +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ + + + ); +}; + +export default ToolPanelModePrompt; diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 98a0fd37d..076330eda 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -14,11 +14,14 @@ import { getDefaultWorkbench } from '../types/workbench'; import { filterToolRegistryByQuery } from '../utils/toolSearch'; // State interface +type ToolPanelMode = 'sidebar' | 'legacy'; + interface ToolWorkflowState { // UI State sidebarsVisible: boolean; leftPanelView: 'toolPicker' | 'toolContent' | 'hidden'; readerMode: boolean; + toolPanelMode: ToolPanelMode; // File/Preview State previewFile: File | null; @@ -33,13 +36,29 @@ type ToolWorkflowAction = | { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean } | { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' } | { type: 'SET_READER_MODE'; payload: boolean } + | { type: 'SET_TOOL_PANEL_MODE'; payload: ToolPanelMode } | { type: 'SET_PREVIEW_FILE'; payload: File | null } | { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null } | { type: 'SET_SEARCH_QUERY'; payload: string } | { type: 'RESET_UI_STATE' }; // Initial state -const initialState: ToolWorkflowState = { +export const TOOL_PANEL_MODE_STORAGE_KEY = 'toolPanelModePreference'; + +const getStoredToolPanelMode = (): ToolPanelMode => { + if (typeof window === 'undefined') { + return 'sidebar'; + } + + const stored = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY); + if (stored === 'legacy' || stored === 'fullscreen') { + return 'legacy'; + } + + return 'sidebar'; +}; + +const baseState: Omit = { sidebarsVisible: true, leftPanelView: 'toolPicker', readerMode: false, @@ -48,6 +67,11 @@ const initialState: ToolWorkflowState = { searchQuery: '', }; +const createInitialState = (): ToolWorkflowState => ({ + ...baseState, + toolPanelMode: getStoredToolPanelMode(), +}); + // Reducer function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState { switch (action.type) { @@ -57,6 +81,8 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio return { ...state, leftPanelView: action.payload }; case 'SET_READER_MODE': return { ...state, readerMode: action.payload }; + case 'SET_TOOL_PANEL_MODE': + return { ...state, toolPanelMode: action.payload }; case 'SET_PREVIEW_FILE': return { ...state, previewFile: action.payload }; case 'SET_PAGE_EDITOR_FUNCTIONS': @@ -64,7 +90,11 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio case 'SET_SEARCH_QUERY': return { ...state, searchQuery: action.payload }; case 'RESET_UI_STATE': - return { ...initialState, searchQuery: state.searchQuery }; // Preserve search + return { + ...baseState, + toolPanelMode: state.toolPanelMode, + searchQuery: state.searchQuery, + }; default: return state; } @@ -82,6 +112,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { setSidebarsVisible: (visible: boolean) => void; setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void; setReaderMode: (mode: boolean) => void; + setToolPanelMode: (mode: ToolPanelMode) => void; setPreviewFile: (file: File | null) => void; setPageEditorFunctions: (functions: PageEditorFunctions | null) => void; setSearchQuery: (query: string) => void; @@ -113,7 +144,7 @@ interface ToolWorkflowProviderProps { } export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { - const [state, dispatch] = useReducer(toolWorkflowReducer, initialState); + const [state, dispatch] = useReducer(toolWorkflowReducer, undefined, createInitialState); // Store reset functions for tools const [toolResetFunctions, setToolResetFunctions] = React.useState void>>({}); @@ -148,6 +179,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { dispatch({ type: 'SET_READER_MODE', payload: mode }); }, [actions]); + const setToolPanelMode = useCallback((mode: ToolPanelMode) => { + dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode }); + }, []); + const setPreviewFile = useCallback((file: File | null) => { dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); if (file) { @@ -163,6 +198,14 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { dispatch({ type: 'SET_SEARCH_QUERY', payload: query }); }, []); + React.useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, state.toolPanelMode); + }, [state.toolPanelMode]); + // Tool reset methods const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => { setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction })); @@ -261,6 +304,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setSidebarsVisible, setLeftPanelView, setReaderMode, + setToolPanelMode, setPreviewFile, setPageEditorFunctions, setSearchQuery, @@ -289,6 +333,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setSidebarsVisible, setLeftPanelView, setReaderMode, + setToolPanelMode, setPreviewFile, setPageEditorFunctions, setSearchQuery, diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 4b9514a3c..bb1a4e482 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -16,6 +16,7 @@ import FileManager from "../components/FileManager"; import LocalIcon from "../components/shared/LocalIcon"; import { useFilesModalContext } from "../contexts/FilesModalContext"; import AppConfigModal from "../components/shared/AppConfigModal"; +import ToolPanelModePrompt from "../components/tools/ToolPanelModePrompt"; import "./HomePage.css"; @@ -30,7 +31,12 @@ export default function HomePage() { const { quickAccessRef } = sidebarRefs; - const { selectedTool, selectedToolKey, handleToolSelect, handleBackToTools } = useToolWorkflow(); + const { + selectedTool, + selectedToolKey, + handleToolSelect, + handleBackToTools, + } = useToolWorkflow(); const { openFilesModal } = useFilesModalContext(); const { colorScheme } = useMantineColorScheme(); @@ -126,6 +132,7 @@ export default function HomePage() { return (
+ {isMobile ? (
@@ -231,8 +238,7 @@ export default function HomePage() { h="100%" className="flex-nowrap flex" > - + diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css index 3b0ac694b..764ec5c74 100644 --- a/frontend/src/styles/theme.css +++ b/frontend/src/styles/theme.css @@ -445,4 +445,11 @@ /* Smooth transitions for theme switching */ * { transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; -} \ No newline at end of file +} +:root { + --shadow-color: rgba(15, 23, 42, 0.55); +} + +[data-theme="dark"] { + --shadow-color: rgba(0, 0, 0, 0.75); +}