diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 405643877..3aa0cbe0f 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -1958,3 +1958,24 @@ viewer.nextPage=Next Page viewer.pageNavigation=Page Navigation viewer.currentPage=Current Page viewer.totalPages=Total Pages + +# Tool panel view modes +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.close=Close +toolPanel.overlay.totalLabel_one={{count}} tool available +toolPanel.overlay.totalLabel_other={{count}} tools available +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. +toolPanel.modePrompt.legacyTitle=Legacy fullscreen +toolPanel.modePrompt.legacyBadge=Not recommended +toolPanel.modePrompt.legacyDescription=Open a fullscreen catalog of tools that hides the workspace until a tool is chosen. +toolPanel.modePrompt.chooseLegacy=Use legacy fullscreen mode +toolPanel.modePrompt.advancedTitle=Advanced sidebar +toolPanel.modePrompt.advancedBadge=Recommended +toolPanel.modePrompt.advancedDescription=Stay in the enhanced sidebar with quick access to tools alongside your workspace. +toolPanel.modePrompt.chooseAdvanced=Use advanced sidebar mode +toolPanel.modePrompt.dismiss=Maybe later diff --git a/frontend/src/components/tools/ToolPanel.css b/frontend/src/components/tools/ToolPanel.css new file mode 100644 index 000000000..bed84fd0b --- /dev/null +++ b/frontend/src/components/tools/ToolPanel.css @@ -0,0 +1,28 @@ +.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; +} + +.tool-panel__mode-toggle { + transition: transform 0.2s ease; +} + +.tool-panel__mode-toggle:hover { + 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); + background: var(--bg-background); +} diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index beefc2c45..ddd5b9819 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -6,20 +6,23 @@ 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, Group, ScrollArea, Text, 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 './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 isMobile = useMediaQuery('(max-width: 1024px)'); - - // Use context-based hooks to eliminate prop drilling const { leftPanelView, isPanelVisible, @@ -27,10 +30,23 @@ export default function ToolPanel() { filteredTools, toolRegistry, setSearchQuery, + toolPanelMode, + setToolPanelMode, + selectedToolKey, + handleToolSelect, + setPreviewFile, } = useToolWorkflow(); - const { selectedToolKey, handleToolSelect } = useToolWorkflow(); - const { setPreviewFile } = useToolWorkflow(); + const isFullscreenMode = toolPanelMode === 'fullscreen'; + const overlayActive = isFullscreenMode && leftPanelView === 'toolPicker' && !isMobile; + + const toggleLabel = isFullscreenMode + ? t('toolPanel.modeToggle.sidebar', 'Switch to advanced sidebar') + : t('toolPanel.modeToggle.fullscreen', 'Switch to legacy fullscreen'); + + const handleToggleMode = () => { + setToolPanelMode(isFullscreenMode ? 'sidebar' : 'fullscreen'); + }; return (
{/* Search Bar - Always visible at the top */}
+ {!isMobile && ( + + + {isFullscreenMode ? ( + + ) : ( + + )} + + + )}
{searchQuery.trim().length > 0 ? ( @@ -81,6 +127,15 @@ export default function ToolPanel() { ) : leftPanelView === 'toolPicker' ? ( // Tool Picker View
+ {isFullscreenMode && !isMobile ? ( +
+ + + {t('toolPanel.overlayHint', 'Select a tool to open it in the workspace.')} + + +
+ ) : null} handleToolSelect(id as ToolId)} diff --git a/frontend/src/components/tools/ToolPanelModePrompt.css b/frontend/src/components/tools/ToolPanelModePrompt.css new file mode 100644 index 000000000..75dfd7093 --- /dev/null +++ b/frontend/src/components/tools/ToolPanelModePrompt.css @@ -0,0 +1,64 @@ +.tool-panel-mode-prompt__previews { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); +} + +.tool-panel-mode-prompt__card { + height: 100%; +} + +.tool-panel-mode-prompt__preview { + border-radius: 0.75rem; + border: 1px solid var(--border-subtle); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(240, 240, 255, 0.6)); + padding: 0.75rem; + display: flex; + gap: 0.75rem; + min-height: 7.5rem; +} + +.tool-panel-mode-prompt__preview--sidebar { + align-items: stretch; +} + +.tool-panel-mode-prompt__preview-sidebar { + width: 3.25rem; + border-radius: 0.5rem; + background: linear-gradient(180deg, var(--accent-ghost), rgba(100, 116, 139, 0.35)); +} + +.tool-panel-mode-prompt__preview-canvas { + flex: 1; + border-radius: 0.5rem; + background: repeating-linear-gradient( + 135deg, + rgba(255, 255, 255, 0.9), + rgba(255, 255, 255, 0.9) 12px, + rgba(226, 232, 240, 0.7) 12px, + rgba(226, 232, 240, 0.7) 24px + ); +} + +.tool-panel-mode-prompt__preview--fullscreen { + justify-content: center; + align-items: center; +} + +.tool-panel-mode-prompt__preview-grid { + width: 100%; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.5rem; +} + +.tool-panel-mode-prompt__preview-grid span { + display: block; + aspect-ratio: 4 / 3; + border-radius: 0.5rem; + background: linear-gradient(180deg, rgba(79, 70, 229, 0.2), rgba(14, 165, 233, 0.35)); +} + +.tool-panel-mode-prompt__card--recommended { + border: 1px solid rgba(236, 72, 153, 0.4); +} diff --git a/frontend/src/components/tools/ToolPanelModePrompt.tsx b/frontend/src/components/tools/ToolPanelModePrompt.tsx new file mode 100644 index 000000000..1503decc8 --- /dev/null +++ b/frontend/src/components/tools/ToolPanelModePrompt.tsx @@ -0,0 +1,124 @@ +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_KEY } from '../../contexts/ToolWorkflowContext'; +import './ToolPanelModePrompt.css'; + +type ToolPanelModeOption = 'sidebar' | 'fullscreen'; + +const PROMPT_STORAGE_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_STORAGE_KEY); + const hasStoredPreference = window.localStorage.getItem(TOOL_PANEL_MODE_KEY); + if (!hasSeenPrompt && !hasStoredPreference) { + setOpened(true); + } + setHydrated(true); + }, []); + + const persistPromptState = () => { + if (typeof window !== 'undefined') { + window.localStorage.setItem(PROMPT_STORAGE_KEY, 'true'); + } + }; + + const handleClose = () => { + persistPromptState(); + setOpened(false); + }; + + const handleSelect = (mode: ToolPanelModeOption) => { + setToolPanelMode(mode); + persistPromptState(); + setOpened(false); + }; + + if (!hydrated) { + return null; + } + + return ( + + + + {t('toolPanel.modePrompt.description', 'Preview both layouts and choose how you want to explore Stirling PDF tools.')} + +
+ + + + {t('toolPanel.modePrompt.advancedTitle', 'Advanced sidebar')} + + {t('toolPanel.modePrompt.advancedBadge', 'Recommended')} + + + + {t('toolPanel.modePrompt.advancedDescription', 'Stay in the enhanced sidebar with quick access to tools alongside your workspace.')} + +
+
+
+
+ + + + + + + {t('toolPanel.modePrompt.legacyTitle', 'Legacy fullscreen')} + + {t('toolPanel.modePrompt.legacyBadge', 'Not recommended')} + + + + {t('toolPanel.modePrompt.legacyDescription', 'Open a fullscreen catalog of tools that hides the workspace until a tool is chosen.')} + +
+
+ + + + +
+
+ +
+
+
+ + + + ); +}; + +export default ToolPanelModePrompt; diff --git a/frontend/src/components/tools/ToolPanelOverlay.css b/frontend/src/components/tools/ToolPanelOverlay.css new file mode 100644 index 000000000..e46948e46 --- /dev/null +++ b/frontend/src/components/tools/ToolPanelOverlay.css @@ -0,0 +1,86 @@ +.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(-10%); + pointer-events: none; +} + +.tool-panel-overlay__paper { + flex: 1; + max-width: 72rem; + margin: 2rem; + display: flex; + flex-direction: column; + background: var(--bg-background); + border-radius: 1.25rem; + overflow: hidden; +} + +.tool-panel-overlay__header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + gap: 1rem; + border-bottom: 1px solid var(--border-subtle); +} + +.tool-panel-overlay__search { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-subtle); + background: var(--bg-toolbar); +} + +.tool-panel-overlay__search-input .search-input-container { + width: min(32rem, 100%); +} + +.tool-panel-overlay__body { + flex: 1; + min-height: 0; + position: relative; +} + +.tool-panel-overlay__scroll { + height: 100%; +} + +.tool-panel-overlay__results, +.tool-panel-overlay__picker { + padding: 1.5rem; + min-height: 100%; +} + +.tool-panel-overlay__picker { + display: flex; + flex-direction: column; + background: var(--bg-toolbar); + border-radius: 1rem; +} + +.tool-panel-overlay__picker .tool-picker-scrollable { + padding-bottom: 2rem; +} + +@media (max-width: 1024px) { + .tool-panel-overlay__paper { + margin: 0; + border-radius: 0; + } +} diff --git a/frontend/src/components/tools/ToolPanelOverlay.tsx b/frontend/src/components/tools/ToolPanelOverlay.tsx new file mode 100644 index 000000000..c267b2613 --- /dev/null +++ b/frontend/src/components/tools/ToolPanelOverlay.tsx @@ -0,0 +1,176 @@ +import { useEffect, useMemo, useState } from 'react'; +import { ActionIcon, Badge, Group, Paper, ScrollArea, 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 './ToolPanelOverlay.css'; + +interface ToolPanelOverlayProps { + isOpen: boolean; +} + +const EXIT_ANIMATION_MS = 320; + +export default function ToolPanelOverlay({ isOpen }: ToolPanelOverlayProps) { + const { t } = useTranslation(); + const { + searchQuery, + setSearchQuery, + filteredTools, + selectedToolKey, + handleToolSelect, + toolRegistry, + setToolPanelMode, + toolPanelMode, + setLeftPanelView, + } = useToolWorkflow(); + + const [shouldRender, setShouldRender] = useState(isOpen); + const [isClosing, setIsClosing] = useState(false); + + 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; + + if (!shouldRender) { + return null; + } + + const handleClose = () => { + setSearchQuery(''); + setLeftPanelView('hidden'); + }; + + const toggleLabel = toolPanelMode === 'fullscreen' + ? t('toolPanel.modeToggle.sidebar', 'Switch to advanced sidebar') + : t('toolPanel.modeToggle.fullscreen', 'Switch to legacy fullscreen'); + + return ( +
+ +
+
+ + {t('toolPanel.overlay.title', 'All tools')} + + + {t('toolPanel.overlay.subtitle', 'Browse and launch tools in the legacy fullscreen catalog.')} + +
+ + + setToolPanelMode(toolPanelMode === 'fullscreen' ? 'sidebar' : 'fullscreen')} + aria-label={toggleLabel} + > + {toolPanelMode === 'fullscreen' ? ( + + ) : ( + + )} + + + + + + + + +
+ +
+ +
+ +
+ + {t('toolPanel.overlay.totalLabel', '{{count}} tools available', { + count: totalToolCount, + })} + +
+
+ +
+ + {showSearchResults ? ( +
+ handleToolSelect(id as ToolId)} + searchQuery={searchQuery} + /> +
+ ) : ( +
+ handleToolSelect(id as ToolId)} + filteredTools={filteredTools} + isSearching={showSearchResults} + /> +
+ )} +
+
+
+
+ ); +} diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 98a0fd37d..ae8acd393 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' | 'fullscreen'; + interface ToolWorkflowState { // UI State sidebarsVisible: boolean; leftPanelView: 'toolPicker' | 'toolContent' | 'hidden'; readerMode: boolean; + toolPanelMode: ToolPanelMode; // File/Preview State previewFile: File | null; @@ -33,13 +36,25 @@ 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_KEY = 'toolPanelModePreference'; + +const getInitialToolPanelMode = (): ToolPanelMode => { + if (typeof window === 'undefined') { + return 'sidebar'; + } + + const storedValue = window.localStorage.getItem(TOOL_PANEL_MODE_KEY); + return storedValue === 'fullscreen' ? 'fullscreen' : 'sidebar'; +}; + +const baseInitialState: Omit = { sidebarsVisible: true, leftPanelView: 'toolPicker', readerMode: false, @@ -48,6 +63,11 @@ const initialState: ToolWorkflowState = { searchQuery: '', }; +const createInitialState = (): ToolWorkflowState => ({ + ...baseInitialState, + toolPanelMode: getInitialToolPanelMode(), +}); + // Reducer function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState { switch (action.type) { @@ -57,6 +77,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 +86,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 { + ...baseInitialState, + searchQuery: state.searchQuery, + toolPanelMode: state.toolPanelMode, + }; default: return state; } @@ -85,6 +111,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { setPreviewFile: (file: File | null) => void; setPageEditorFunctions: (functions: PageEditorFunctions | null) => void; setSearchQuery: (query: string) => void; + setToolPanelMode: (mode: ToolPanelMode) => void; // Tool Actions selectTool: (toolId: ToolId | null) => void; @@ -113,7 +140,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>>({}); @@ -163,6 +190,15 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { dispatch({ type: 'SET_SEARCH_QUERY', payload: query }); }, []); + const setToolPanelMode = useCallback((mode: ToolPanelMode) => { + dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode }); + }, []); + + React.useEffect(() => { + if (typeof window === 'undefined') return; + window.localStorage.setItem(TOOL_PANEL_MODE_KEY, state.toolPanelMode); + }, [state.toolPanelMode]); + // Tool reset methods const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => { setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction })); @@ -264,6 +300,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setPreviewFile, setPageEditorFunctions, setSearchQuery, + setToolPanelMode, selectTool: actions.setSelectedTool, clearToolSelection: () => actions.setSelectedTool(null), @@ -292,6 +329,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setPreviewFile, setPageEditorFunctions, setSearchQuery, + setToolPanelMode, actions.setSelectedTool, registerToolReset, resetTool, diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 4b9514a3c..c6b3fb8dc 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -9,6 +9,8 @@ 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"; import RightRail from "../components/shared/RightRail"; @@ -30,7 +32,14 @@ export default function HomePage() { const { quickAccessRef } = sidebarRefs; - const { selectedTool, selectedToolKey, handleToolSelect, handleBackToTools } = useToolWorkflow(); + const { + selectedTool, + selectedToolKey, + handleToolSelect, + handleBackToTools, + leftPanelView, + toolPanelMode, + } = useToolWorkflow(); const { openFilesModal } = useFilesModalContext(); const { colorScheme } = useMantineColorScheme(); @@ -124,8 +133,12 @@ export default function HomePage() { // Note: File selection limits are now handled directly by individual tools + const showFullscreenOverlay = !isMobile && toolPanelMode === 'fullscreen' && leftPanelView === 'toolPicker'; + return (
+ + {isMobile ? (