= ({ setActiveButton })
const { getHomeNavigation } = useSidebarNavigation();
// Determine if the indicator should be visible (do not require selectedTool to be resolved yet)
+ // Special case: multiTool should always show even when sidebars are hidden
const indicatorShouldShow = Boolean(
- selectedToolKey && leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)
+ selectedToolKey &&
+ ((leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)) ||
+ selectedToolKey === 'multiTool')
);
// Local animation and hover state
diff --git a/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts b/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts
index efadc4a72..03e028834 100644
--- a/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts
+++ b/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts
@@ -12,7 +12,7 @@ export const isNavButtonActive = (
isFilesModalOpen: boolean,
configModalOpen: boolean,
selectedToolKey?: string | null,
- leftPanelView?: 'toolPicker' | 'toolContent'
+ leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
): boolean => {
const isActiveByLocalState = config.type === 'navigation' && activeButton === config.id;
const isActiveByContext =
@@ -35,7 +35,7 @@ export const getNavButtonStyle = (
isFilesModalOpen: boolean,
configModalOpen: boolean,
selectedToolKey?: string | null,
- leftPanelView?: 'toolPicker' | 'toolContent'
+ leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
) => {
const isActive = isNavButtonActive(
config,
diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx
index d3eea3bd9..7f19482bd 100644
--- a/frontend/src/components/tools/ToolPanel.tsx
+++ b/frontend/src/components/tools/ToolPanel.tsx
@@ -7,6 +7,7 @@ import ToolSearch from './toolPicker/ToolSearch';
import { useSidebarContext } from "../../contexts/SidebarContext";
import rainbowStyles from '../../styles/rainbow.module.css';
import { ScrollArea } from '@mantine/core';
+import { ToolId } from '../../types/toolId';
// No props needed - component uses context
@@ -71,7 +72,7 @@ export default function ToolPanel() {
handleToolSelect(id as ToolId)}
searchQuery={searchQuery}
/>
@@ -80,7 +81,7 @@ export default function ToolPanel() {
handleToolSelect(id as ToolId)}
filteredTools={filteredTools}
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
/>
diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx
index 9f3d60d50..236cbb49f 100644
--- a/frontend/src/components/tools/toolPicker/ToolButton.tsx
+++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx
@@ -17,7 +17,8 @@ interface ToolButtonProps {
}
const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
- const isUnavailable = !tool.component && !tool.link;
+ // Special case: read and multiTool are navigational tools that are always available
+ const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
const { getToolNavigation } = useToolNavigation();
const handleClick = (id: string) => {
diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx
index f9e8ba9a5..b71664b6b 100644
--- a/frontend/src/contexts/NavigationContext.tsx
+++ b/frontend/src/contexts/NavigationContext.tsx
@@ -109,16 +109,34 @@ export const NavigationProvider: React.FC<{
const actions: NavigationContextActions = {
setWorkbench: useCallback((workbench: WorkbenchType) => {
- dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
- }, []),
+ // If we're leaving pageEditor workbench and have unsaved changes, request navigation
+ if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
+ const performWorkbenchChange = () => {
+ dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
+ };
+ dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
+ dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
+ } else {
+ dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
+ }
+ }, [state.workbench, state.hasUnsavedChanges]),
setSelectedTool: useCallback((toolId: ToolId | null) => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
}, []),
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
- dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
- }, []),
+ // If we're leaving pageEditor workbench and have unsaved changes, request navigation
+ if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
+ const performWorkbenchChange = () => {
+ dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
+ };
+ dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
+ dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
+ } else {
+ dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
+ }
+ }, [state.workbench, state.hasUnsavedChanges]),
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx
index 106636692..e38375daa 100644
--- a/frontend/src/contexts/ToolWorkflowContext.tsx
+++ b/frontend/src/contexts/ToolWorkflowContext.tsx
@@ -17,7 +17,7 @@ import { filterToolRegistryByQuery } from '../utils/toolSearch';
interface ToolWorkflowState {
// UI State
sidebarsVisible: boolean;
- leftPanelView: 'toolPicker' | 'toolContent';
+ leftPanelView: 'toolPicker' | 'toolContent' | 'hidden';
readerMode: boolean;
// File/Preview State
@@ -31,7 +31,7 @@ interface ToolWorkflowState {
// Actions
type ToolWorkflowAction =
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
- | { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' }
+ | { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' }
| { type: 'SET_READER_MODE'; payload: boolean }
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
@@ -80,7 +80,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
// UI Actions
setSidebarsVisible: (visible: boolean) => void;
- setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
+ setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void;
setReaderMode: (mode: boolean) => void;
setPreviewFile: (file: File | null) => void;
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
@@ -96,7 +96,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
resetTool: (toolId: string) => void;
// Workflow Actions (compound actions)
- handleToolSelect: (toolId: string) => void;
+ handleToolSelect: (toolId: ToolId) => void;
handleBackToTools: () => void;
handleReaderToggle: () => void;
@@ -136,7 +136,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
}, []);
- const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => {
+ const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent' | 'hidden') => {
dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view });
}, []);
@@ -180,7 +180,26 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
}, []); // Empty dependency array makes this stable
// Workflow actions (compound actions that coordinate multiple state changes)
- const handleToolSelect = useCallback((toolId: string) => {
+ const handleToolSelect = useCallback((toolId: ToolId) => {
+ // Handle read tool selection - should behave exactly like QuickAccessBar read button
+ if (toolId === 'read') {
+ setReaderMode(true);
+ actions.setSelectedTool('read');
+ actions.setWorkbench('viewer');
+ setSearchQuery('');
+ return;
+ }
+
+ // Handle multiTool selection - enable page editor workbench and hide left panel
+ if (toolId === 'multiTool') {
+ setReaderMode(false);
+ setLeftPanelView('hidden');
+ actions.setSelectedTool('multiTool');
+ actions.setWorkbench('pageEditor');
+ setSearchQuery('');
+ return;
+ }
+
// Set the selected tool and determine the appropriate workbench
const validToolId = isValidToolId(toolId) ? toolId : null;
actions.setSelectedTool(validToolId);
@@ -195,19 +214,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Clear search query when selecting a tool
setSearchQuery('');
-
- // Handle view switching logic
- if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
- setLeftPanelView('toolPicker');
- if (toolId === 'read' || toolId === 'view-pdf') {
- setReaderMode(true);
- } else {
- setReaderMode(false);
- }
- } else {
- setLeftPanelView('toolContent');
- setReaderMode(false); // Disable read mode when selecting tools
- }
+ setLeftPanelView('toolContent');
+ setReaderMode(false); // Disable read mode when selecting tools
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
const handleBackToTools = useCallback(() => {
@@ -227,8 +235,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
}, [toolRegistry, state.searchQuery]);
const isPanelVisible = useMemo(() =>
- state.sidebarsVisible && !state.readerMode,
- [state.sidebarsVisible, state.readerMode]
+ state.sidebarsVisible && !state.readerMode && state.leftPanelView !== 'hidden',
+ [state.sidebarsVisible, state.readerMode, state.leftPanelView]
);
// URL sync for proper tool navigation
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
index c8c29821e..c36f22b2d 100644
--- a/frontend/src/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -183,8 +183,32 @@ export function useFlatToolRegistry(): ToolRegistry {
return useMemo(() => {
const allTools: ToolRegistry = {
+ // Recommended Tools in order
+ multiTool: {
+ icon: ,
+ name: t("home.multiTool.title", "Multi-Tool"),
+ component: null,
+ workbench: "pageEditor",
+ description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
+ categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
+ subcategoryId: SubcategoryId.GENERAL,
+ maxFiles: -1,
+ synonyms: getSynonyms(t, "multiTool"),
+ },
+ merge: {
+ icon: ,
+ name: t("home.merge.title", "Merge"),
+ component: Merge,
+ description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
+ categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
+ subcategoryId: SubcategoryId.GENERAL,
+ maxFiles: -1,
+ endpoints: ["merge-pdfs"],
+ operationConfig: mergeOperationConfig,
+ settingsComponent: MergeSettings,
+ synonyms: getSynonyms(t, "merge")
+ },
// Signing
-
certSign: {
icon: ,
name: t("home.certSign.title", "Certificate Sign"),
@@ -792,30 +816,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: ConvertSettings,
synonyms: getSynonyms(t, "convert")
},
- merge: {
- icon: ,
- name: t("home.merge.title", "Merge"),
- component: Merge,
- description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
- categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
- subcategoryId: SubcategoryId.GENERAL,
- maxFiles: -1,
- endpoints: ["merge-pdfs"],
- operationConfig: mergeOperationConfig,
- settingsComponent: MergeSettings,
- synonyms: getSynonyms(t, "merge")
- },
- multiTool: {
- icon: ,
- name: t("home.multiTool.title", "Multi-Tool"),
- component: null,
- workbench: "pageEditor",
- description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
- categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
- subcategoryId: SubcategoryId.GENERAL,
- maxFiles: -1,
- synonyms: getSynonyms(t, "multiTool"),
- },
+
ocr: {
icon: ,
name: t("home.ocr.title", "OCR"),
diff --git a/frontend/src/hooks/useToolNavigation.ts b/frontend/src/hooks/useToolNavigation.ts
index 704fd5026..5c6fcf47c 100644
--- a/frontend/src/hooks/useToolNavigation.ts
+++ b/frontend/src/hooks/useToolNavigation.ts
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { ToolRegistryEntry, getToolUrlPath } from '../data/toolsTaxonomy';
import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
import { handleUnlessSpecialClick } from '../utils/clickHandlers';
+import { ToolId } from '../types/toolId';
export interface ToolNavigationProps {
/** Full URL for the tool (for href attribute) */
@@ -34,7 +35,7 @@ export function useToolNavigation(): {
}
// Use SPA navigation for internal tools
- handleToolSelect(toolId);
+ handleToolSelect(toolId as ToolId);
});
};
@@ -42,4 +43,4 @@ export function useToolNavigation(): {
}, [handleToolSelect]);
return { getToolNavigation };
-}
\ No newline at end of file
+}
diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts
index 088a1ba52..5c5be8773 100644
--- a/frontend/src/hooks/useToolSections.ts
+++ b/frontend/src/hooks/useToolSections.ts
@@ -72,7 +72,10 @@ export function useToolSections(
const subcategoryId = s as SubcategoryId;
if (!quick[subcategoryId]) quick[subcategoryId] = [];
// Only include ready tools (have a component or external link) in Quick Access
- const readyTools = tools.filter(({ tool }) => tool.component !== null || !!tool.link);
+ // Special case: read and multiTool are navigational tools that don't need components
+ const readyTools = tools.filter(({ tool, id }) =>
+ tool.component !== null || !!tool.link || id === 'read' || id === 'multiTool'
+ );
quick[subcategoryId].push(...readyTools);
});
}
diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts
index a90ee4fbe..545285c5e 100644
--- a/frontend/src/hooks/useUrlSync.ts
+++ b/frontend/src/hooks/useUrlSync.ts
@@ -14,7 +14,7 @@ import { withBasePath } from '../constants/app';
*/
export function useNavigationUrlSync(
selectedTool: ToolId | null,
- handleToolSelect: (toolId: string) => void,
+ handleToolSelect: (toolId: ToolId) => void,
clearToolSelection: () => void,
registry: ToolRegistry,
enableSync: boolean = true