Rework navigation

This commit is contained in:
Connor Yoh 2025-08-28 10:54:48 +01:00
parent 83400dc6a7
commit e59e73ceb0
22 changed files with 654 additions and 706 deletions

View File

@ -7,6 +7,7 @@ import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
import { SidebarProvider } from "./contexts/SidebarContext"; import { SidebarProvider } from "./contexts/SidebarContext";
import ErrorBoundary from "./components/shared/ErrorBoundary"; import ErrorBoundary from "./components/shared/ErrorBoundary";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import { ScarfPixel } from "./components/ScarfPixel";
// Import global styles // Import global styles
import "./styles/tailwind.css"; import "./styles/tailwind.css";
@ -36,6 +37,7 @@ export default function App() {
<ErrorBoundary> <ErrorBoundary>
<FileContextProvider enableUrlSync={true} enablePersistence={true}> <FileContextProvider enableUrlSync={true} enablePersistence={true}>
<NavigationProvider> <NavigationProvider>
<ScarfPixel />
<FilesModalProvider> <FilesModalProvider>
<ToolWorkflowProvider> <ToolWorkflowProvider>
<SidebarProvider> <SidebarProvider>

View File

@ -1,22 +1,19 @@
import { useLocation } from "react-router-dom";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useNavigationState } from "../contexts/NavigationContext";
export function ScarfPixel() { export function ScarfPixel() {
const location = useLocation(); const { workbench, selectedTool } = useNavigationState();
const lastUrlSent = useRef<string | null>(null); // helps with React 18 StrictMode in dev const lastUrlSent = useRef<string | null>(null); // helps with React 18 StrictMode in dev
useEffect(() => { useEffect(() => {
// Force reload of the tracking pixel on route change // Get current pathname from browser location
const pathname = window.location.pathname;
const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7' const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7'
+ '&path=' + encodeURIComponent(location.pathname) + '&path=' + encodeURIComponent(pathname)
+ '&t=' + Date.now(); // cache-buster + '&t=' + Date.now(); // cache-buster
console.log("ScarfPixel: Navigation change", { workbench, selectedTool, pathname });
// + '&machineType=' + machineType
// + '&appVersion=' + appVersion
// + '&licenseType=' + license
// + '&loginEnabled=' + loginEnabled;
console.log("ScarfPixel: reload " + location.pathname );
if (lastUrlSent.current !== url) { if (lastUrlSent.current !== url) {
lastUrlSent.current = url; lastUrlSent.current = url;
@ -24,9 +21,9 @@ export function ScarfPixel() {
img.referrerPolicy = "no-referrer-when-downgrade"; // optional img.referrerPolicy = "no-referrer-when-downgrade"; // optional
img.src = url; img.src = url;
console.log("ScarfPixel: Fire to... " + location.pathname , url); console.log("ScarfPixel: Fire to... " + pathname, url);
} }
}, [location.pathname]); }, [workbench, selectedTool]); // Fire when navigation state changes
return null; // Nothing visible in UI return null; // Nothing visible in UI
} }

View File

@ -411,9 +411,9 @@ const FileEditor = ({
if (record) { if (record) {
// Set the file as selected in context and switch to viewer for preview // Set the file as selected in context and switch to viewer for preview
setSelectedFiles([fileId]); setSelectedFiles([fileId]);
navActions.setMode('viewer'); navActions.setWorkbench('viewer');
} }
}, [activeFileRecords, setSelectedFiles, navActions.setMode]); }, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
const handleMergeFromHere = useCallback((fileId: string) => { const handleMergeFromHere = useCallback((fileId: string) => {
const startIndex = activeFileRecords.findIndex(r => r.id === fileId); const startIndex = activeFileRecords.findIndex(r => r.id === fileId);

View File

@ -6,13 +6,13 @@ import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useFileHandler } from '../../hooks/useFileHandler'; import { useFileHandler } from '../../hooks/useFileHandler';
import { useFileState, useFileActions } from '../../contexts/FileContext'; import { useFileState, useFileActions } from '../../contexts/FileContext';
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext'; import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
import { useToolManagement } from '../../hooks/useToolManagement';
import TopControls from '../shared/TopControls'; import TopControls from '../shared/TopControls';
import FileEditor from '../fileEditor/FileEditor'; import FileEditor from '../fileEditor/FileEditor';
import PageEditor from '../pageEditor/PageEditor'; import PageEditor from '../pageEditor/PageEditor';
import PageEditorControls from '../pageEditor/PageEditorControls'; import PageEditorControls from '../pageEditor/PageEditorControls';
import Viewer from '../viewer/Viewer'; import Viewer from '../viewer/Viewer';
import ToolRenderer from '../tools/ToolRenderer';
import LandingPage from '../shared/LandingPage'; import LandingPage from '../shared/LandingPage';
// No props needed - component uses contexts directly // No props needed - component uses contexts directly
@ -23,9 +23,9 @@ export default function Workbench() {
// Use context-based hooks to eliminate all prop drilling // Use context-based hooks to eliminate all prop drilling
const { state } = useFileState(); const { state } = useFileState();
const { actions } = useFileActions(); const { actions } = useFileActions();
const { currentMode: currentView } = useNavigationState(); const { workbench: currentView } = useNavigationState();
const { actions: navActions } = useNavigationActions(); const { actions: navActions } = useNavigationActions();
const setCurrentView = navActions.setMode; const setCurrentView = navActions.setWorkbench;
const activeFiles = state.files.ids; const activeFiles = state.files.ids;
const { const {
previewFile, previewFile,
@ -36,7 +36,14 @@ export default function Workbench() {
setSidebarsVisible setSidebarsVisible
} = useToolWorkflow(); } = useToolWorkflow();
const { selectedToolKey, selectedTool, handleToolSelect } = useToolWorkflow(); const { handleToolSelect } = useToolWorkflow();
// Get navigation state - this is the source of truth
const { selectedTool: selectedToolId } = useNavigationState();
// Get tool registry to look up selected tool
const { toolRegistry } = useToolManagement();
const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null;
const { addToActiveFiles } = useFileHandler(); const { addToActiveFiles } = useFileHandler();
const handlePreviewClose = () => { const handlePreviewClose = () => {
@ -69,11 +76,11 @@ export default function Workbench() {
case "fileEditor": case "fileEditor":
return ( return (
<FileEditor <FileEditor
toolMode={!!selectedToolKey} toolMode={!!selectedToolId}
showUpload={true} showUpload={true}
showBulkActions={!selectedToolKey} showBulkActions={!selectedToolId}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]} supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
{...(!selectedToolKey && { {...(!selectedToolId && {
onOpenPageEditor: (file) => { onOpenPageEditor: (file) => {
setCurrentView("pageEditor"); setCurrentView("pageEditor");
}, },
@ -127,14 +134,6 @@ export default function Workbench() {
); );
default: default:
// Check if it's a tool view
if (selectedToolKey && selectedTool) {
return (
<ToolRenderer
selectedToolKey={selectedToolKey}
/>
);
}
return ( return (
<LandingPage/> <LandingPage/>
); );
@ -154,7 +153,7 @@ export default function Workbench() {
<TopControls <TopControls
currentView={currentView} currentView={currentView}
setCurrentView={setCurrentView} setCurrentView={setCurrentView}
selectedToolKey={selectedToolKey} selectedToolKey={selectedToolId}
/> />
{/* Main content area */} {/* Main content area */}

View File

@ -6,7 +6,6 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext"; import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext";
import { ModeType } from "../../contexts/NavigationContext";
import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor"; import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing"; import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
import { pdfExportService } from "../../services/pdfExportService"; import { pdfExportService } from "../../services/pdfExportService";

View File

@ -25,7 +25,7 @@ export default function RightRail() {
const [csvInput, setCsvInput] = useState<string>(""); const [csvInput, setCsvInput] = useState<string>("");
// Navigation view // Navigation view
const { currentMode: currentView } = useNavigationState(); const { workbench: currentView } = useNavigationState();
// File state and selection // File state and selection
const { state, selectors } = useFileState(); const { state, selectors } = useFileState();

View File

@ -5,7 +5,7 @@ import rainbowStyles from '../../styles/rainbow.module.css';
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import EditNoteIcon from "@mui/icons-material/EditNote"; import EditNoteIcon from "@mui/icons-material/EditNote";
import FolderIcon from "@mui/icons-material/Folder"; import FolderIcon from "@mui/icons-material/Folder";
import { ModeType, isValidMode } from '../../contexts/NavigationContext'; import { WorkbenchType, isValidWorkbench } from '../../types/navigation';
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
const viewOptionStyle = { const viewOptionStyle = {
@ -19,7 +19,7 @@ const viewOptionStyle = {
// Build view options showing text only for current view; others icon-only with tooltip // Build view options showing text only for current view; others icon-only with tooltip
const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null) => [ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => [
{ {
label: ( label: (
<div style={viewOptionStyle as React.CSSProperties}> <div style={viewOptionStyle as React.CSSProperties}>
@ -70,8 +70,8 @@ const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null)
]; ];
interface TopControlsProps { interface TopControlsProps {
currentView: ModeType; currentView: WorkbenchType;
setCurrentView: (view: ModeType) => void; setCurrentView: (view: WorkbenchType) => void;
selectedToolKey?: string | null; selectedToolKey?: string | null;
} }
@ -81,25 +81,25 @@ const TopControls = ({
selectedToolKey, selectedToolKey,
}: TopControlsProps) => { }: TopControlsProps) => {
const { isRainbowMode } = useRainbowThemeContext(); const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<ModeType | null>(null); const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
const isToolSelected = selectedToolKey !== null; const isToolSelected = selectedToolKey !== null;
const handleViewChange = useCallback((view: string) => { const handleViewChange = useCallback((view: string) => {
if (!isValidMode(view)) { if (!isValidWorkbench(view)) {
// Ignore invalid values defensively
return; return;
} }
const mode = view as ModeType;
const workbench = view;
// Show immediate feedback // Show immediate feedback
setSwitchingTo(mode as ModeType); setSwitchingTo(workbench);
// Defer the heavy view change to next frame so spinner can render // Defer the heavy view change to next frame so spinner can render
requestAnimationFrame(() => { requestAnimationFrame(() => {
// Give the spinner one more frame to show // Give the spinner one more frame to show
requestAnimationFrame(() => { requestAnimationFrame(() => {
setCurrentView(mode as ModeType); setCurrentView(workbench);
// Clear the loading state after view change completes // Clear the loading state after view change completes
setTimeout(() => setSwitchingTo(null), 300); setTimeout(() => setSwitchingTo(null), 300);

View File

@ -1,6 +1,6 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react'; import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { useNavigationUrlSync } from '../hooks/useUrlSync'; import { WorkbenchType, ToolId, getDefaultWorkbench } from '../types/navigation';
import { ModeType, isValidMode, getDefaultMode } from '../types/navigation'; import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry';
/** /**
* NavigationContext - Complete navigation management system * NavigationContext - Complete navigation management system
@ -11,27 +11,38 @@ import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
*/ */
// Navigation state // Navigation state
interface NavigationState { interface NavigationContextState {
currentMode: ModeType; workbench: WorkbenchType;
selectedTool: ToolId | null;
hasUnsavedChanges: boolean; hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null; pendingNavigation: (() => void) | null;
showNavigationWarning: boolean; showNavigationWarning: boolean;
selectedToolKey: string | null; // Add tool selection to navigation state
} }
// Navigation actions // Navigation actions
type NavigationAction = type NavigationAction =
| { type: 'SET_MODE'; payload: { mode: ModeType } } | { type: 'SET_WORKBENCH'; payload: { workbench: WorkbenchType } }
| { type: 'SET_SELECTED_TOOL'; payload: { toolId: ToolId | null } }
| { type: 'SET_TOOL_AND_WORKBENCH'; payload: { toolId: ToolId | null; workbench: WorkbenchType } }
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } } | { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } } | { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } } | { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } };
| { type: 'SET_SELECTED_TOOL'; payload: { toolKey: string | null } };
// Navigation reducer // Navigation reducer
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => { const navigationReducer = (state: NavigationContextState, action: NavigationAction): NavigationContextState => {
switch (action.type) { switch (action.type) {
case 'SET_MODE': case 'SET_WORKBENCH':
return { ...state, currentMode: action.payload.mode }; return { ...state, workbench: action.payload.workbench };
case 'SET_SELECTED_TOOL':
return { ...state, selectedTool: action.payload.toolId };
case 'SET_TOOL_AND_WORKBENCH':
return {
...state,
selectedTool: action.payload.toolId,
workbench: action.payload.workbench
};
case 'SET_UNSAVED_CHANGES': case 'SET_UNSAVED_CHANGES':
return { ...state, hasUnsavedChanges: action.payload.hasChanges }; return { ...state, hasUnsavedChanges: action.payload.hasChanges };
@ -42,43 +53,41 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
case 'SHOW_NAVIGATION_WARNING': case 'SHOW_NAVIGATION_WARNING':
return { ...state, showNavigationWarning: action.payload.show }; return { ...state, showNavigationWarning: action.payload.show };
case 'SET_SELECTED_TOOL':
return { ...state, selectedToolKey: action.payload.toolKey };
default: default:
return state; return state;
} }
}; };
// Initial state // Initial state
const initialState: NavigationState = { const initialState: NavigationContextState = {
currentMode: getDefaultMode(), workbench: getDefaultWorkbench(),
selectedTool: null,
hasUnsavedChanges: false, hasUnsavedChanges: false,
pendingNavigation: null, pendingNavigation: null,
showNavigationWarning: false, showNavigationWarning: false
selectedToolKey: null
}; };
// Navigation context actions interface // Navigation context actions interface
export interface NavigationContextActions { export interface NavigationContextActions {
setMode: (mode: ModeType) => void; setWorkbench: (workbench: WorkbenchType) => void;
setSelectedTool: (toolId: ToolId | null) => void;
setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void; setHasUnsavedChanges: (hasChanges: boolean) => void;
showNavigationWarning: (show: boolean) => void; showNavigationWarning: (show: boolean) => void;
requestNavigation: (navigationFn: () => void) => void; requestNavigation: (navigationFn: () => void) => void;
confirmNavigation: () => void; confirmNavigation: () => void;
cancelNavigation: () => void; cancelNavigation: () => void;
selectTool: (toolKey: string) => void;
clearToolSelection: () => void; clearToolSelection: () => void;
handleToolSelect: (toolId: string) => void; handleToolSelect: (toolId: string) => void;
} }
// Split context values // Context state values
export interface NavigationContextStateValue { export interface NavigationContextStateValue {
currentMode: ModeType; workbench: WorkbenchType;
selectedTool: ToolId | null;
hasUnsavedChanges: boolean; hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null; pendingNavigation: (() => void) | null;
showNavigationWarning: boolean; showNavigationWarning: boolean;
selectedToolKey: string | null;
} }
export interface NavigationContextActionsValue { export interface NavigationContextActionsValue {
@ -95,10 +104,19 @@ export const NavigationProvider: React.FC<{
enableUrlSync?: boolean; enableUrlSync?: boolean;
}> = ({ children, enableUrlSync = true }) => { }> = ({ children, enableUrlSync = true }) => {
const [state, dispatch] = useReducer(navigationReducer, initialState); const [state, dispatch] = useReducer(navigationReducer, initialState);
const toolRegistry = useFlatToolRegistry();
const actions: NavigationContextActions = { const actions: NavigationContextActions = {
setMode: useCallback((mode: ModeType) => { setWorkbench: useCallback((workbench: WorkbenchType) => {
dispatch({ type: 'SET_MODE', payload: { mode } }); dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
}, []),
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 } });
}, []), }, []),
setHasUnsavedChanges: useCallback((hasChanges: boolean) => { setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
@ -110,77 +128,64 @@ export const NavigationProvider: React.FC<{
}, []), }, []),
requestNavigation: useCallback((navigationFn: () => void) => { requestNavigation: useCallback((navigationFn: () => void) => {
// If no unsaved changes, navigate immediately
if (!state.hasUnsavedChanges) { if (!state.hasUnsavedChanges) {
navigationFn(); navigationFn();
return; return;
} }
// Otherwise, store the navigation and show warning
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } }); dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
}, [state.hasUnsavedChanges]), }, [state.hasUnsavedChanges]),
confirmNavigation: useCallback(() => { confirmNavigation: useCallback(() => {
// Execute pending navigation
if (state.pendingNavigation) { if (state.pendingNavigation) {
state.pendingNavigation(); state.pendingNavigation();
} }
// Clear navigation state
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
}, [state.pendingNavigation]), }, [state.pendingNavigation]),
cancelNavigation: useCallback(() => { cancelNavigation: useCallback(() => {
// Clear navigation without executing
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
}, []), }, []),
selectTool: useCallback((toolKey: string) => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey } });
}, []),
clearToolSelection: useCallback(() => { clearToolSelection: useCallback(() => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
}, []), }, []),
handleToolSelect: useCallback((toolId: string) => { handleToolSelect: useCallback((toolId: string) => {
// Handle special cases
if (toolId === 'allTools') { if (toolId === 'allTools') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
return; return;
} }
// Special-case: if tool is a dedicated reader tool, enter reader mode
if (toolId === 'read' || toolId === 'view-pdf') { if (toolId === 'read' || toolId === 'view-pdf') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: 'viewer' } });
return; return;
} }
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: toolId } }); // Look up the tool in the registry to get its proper workbench
dispatch({ type: 'SET_MODE', payload: { mode: 'fileEditor' as ModeType } }); const tool = toolRegistry[toolId];
}, []) const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench();
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
}, [toolRegistry])
}; };
const stateValue: NavigationContextStateValue = { const stateValue: NavigationContextStateValue = {
currentMode: state.currentMode, workbench: state.workbench,
selectedTool: state.selectedTool,
hasUnsavedChanges: state.hasUnsavedChanges, hasUnsavedChanges: state.hasUnsavedChanges,
pendingNavigation: state.pendingNavigation, pendingNavigation: state.pendingNavigation,
showNavigationWarning: state.showNavigationWarning, showNavigationWarning: state.showNavigationWarning
selectedToolKey: state.selectedToolKey
}; };
const actionsValue: NavigationContextActionsValue = { const actionsValue: NavigationContextActionsValue = {
actions actions
}; };
// Enable URL synchronization
useNavigationUrlSync(state.currentMode, actions.setMode, enableUrlSync);
return ( return (
<NavigationStateContext.Provider value={stateValue}> <NavigationStateContext.Provider value={stateValue}>
<NavigationActionsContext.Provider value={actionsValue}> <NavigationActionsContext.Provider value={actionsValue}>
@ -231,9 +236,6 @@ export const useNavigationGuard = () => {
}; };
}; };
// Re-export utility functions from types for backward compatibility
export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation';
// TODO: This will be expanded for URL-based routing system // TODO: This will be expanded for URL-based routing system
// - URL parsing utilities // - URL parsing utilities
// - Route definitions // - Route definitions

View File

@ -7,8 +7,8 @@ import React, { createContext, useContext, useReducer, useCallback, useMemo } fr
import { useToolManagement } from '../hooks/useToolManagement'; import { useToolManagement } from '../hooks/useToolManagement';
import { PageEditorFunctions } from '../types/pageEditor'; import { PageEditorFunctions } from '../types/pageEditor';
import { ToolRegistryEntry } from '../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
import { useNavigationActions, useNavigationState } from './NavigationContext'; import { useNavigationActions, useNavigationState } from './NavigationContext';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
// State interface // State interface
interface ToolWorkflowState { interface ToolWorkflowState {
@ -124,7 +124,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
} = useToolManagement(); } = useToolManagement();
// Get selected tool from navigation context // Get selected tool from navigation context
const selectedTool = getSelectedTool(navigationState.selectedToolKey); const selectedTool = getSelectedTool(navigationState.selectedTool);
// UI Action creators // UI Action creators
const setSidebarsVisible = useCallback((visible: boolean) => { const setSidebarsVisible = useCallback((visible: boolean) => {
@ -142,7 +142,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const setPreviewFile = useCallback((file: File | null) => { const setPreviewFile = useCallback((file: File | null) => {
dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
if (file) { if (file) {
actions.setMode('viewer'); actions.setWorkbench('viewer');
} }
}, [actions]); }, [actions]);
@ -172,7 +172,16 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Workflow actions (compound actions that coordinate multiple state changes) // Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: string) => { const handleToolSelect = useCallback((toolId: string) => {
actions.handleToolSelect(toolId); // Set the selected tool and determine the appropriate workbench
actions.setSelectedTool(toolId);
// Get the tool from registry to determine workbench
const tool = getSelectedTool(toolId);
if (tool && tool.workbench) {
actions.setWorkbench(tool.workbench);
} else {
actions.setWorkbench('fileEditor'); // Default workbench
}
// Clear search query when selecting a tool // Clear search query when selecting a tool
setSearchQuery(''); setSearchQuery('');
@ -189,13 +198,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setLeftPanelView('toolContent'); setLeftPanelView('toolContent');
setReaderMode(false); // Disable read mode when selecting tools setReaderMode(false); // Disable read mode when selecting tools
} }
}, [actions, setLeftPanelView, setReaderMode, setSearchQuery]); }, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
const handleBackToTools = useCallback(() => { const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker'); setLeftPanelView('toolPicker');
setReaderMode(false); setReaderMode(false);
actions.clearToolSelection(); actions.setSelectedTool(null);
}, [setLeftPanelView, setReaderMode, actions]); }, [setLeftPanelView, setReaderMode, actions.setSelectedTool]);
const handleReaderToggle = useCallback(() => { const handleReaderToggle = useCallback(() => {
setReaderMode(true); setReaderMode(true);
@ -214,14 +223,23 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
[state.sidebarsVisible, state.readerMode] [state.sidebarsVisible, state.readerMode]
); );
// Enable URL synchronization for tool selection // URL sync for proper tool navigation
useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true); useNavigationUrlSync(
navigationState.workbench,
navigationState.selectedTool,
actions.setWorkbench,
actions.setSelectedTool,
handleToolSelect,
() => actions.setSelectedTool(null),
toolRegistry,
true
);
// Properly memoized context value // Properly memoized context value
const contextValue = useMemo((): ToolWorkflowContextValue => ({ const contextValue = useMemo((): ToolWorkflowContextValue => ({
// State // State
...state, ...state,
selectedToolKey: navigationState.selectedToolKey, selectedToolKey: navigationState.selectedTool,
selectedTool, selectedTool,
toolRegistry, toolRegistry,
@ -232,8 +250,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setPreviewFile, setPreviewFile,
setPageEditorFunctions, setPageEditorFunctions,
setSearchQuery, setSearchQuery,
selectTool: actions.selectTool, selectTool: actions.setSelectedTool,
clearToolSelection: actions.clearToolSelection, clearToolSelection: () => actions.setSelectedTool(null),
// Tool Reset Actions // Tool Reset Actions
registerToolReset, registerToolReset,
@ -249,7 +267,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
isPanelVisible, isPanelVisible,
}), [ }), [
state, state,
navigationState.selectedToolKey, navigationState.selectedTool,
selectedTool, selectedTool,
toolRegistry, toolRegistry,
setSidebarsVisible, setSidebarsVisible,
@ -258,8 +276,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setPreviewFile, setPreviewFile,
setPageEditorFunctions, setPageEditorFunctions,
setSearchQuery, setSearchQuery,
actions.selectTool, actions.setSelectedTool,
actions.clearToolSelection,
registerToolReset, registerToolReset,
resetTool, resetTool,
handleToolSelect, handleToolSelect,

View File

@ -3,6 +3,7 @@ import React from 'react';
import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation'; import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool'; import { BaseToolProps } from '../types/tool';
import { BaseParameters } from '../types/parameters'; import { BaseParameters } from '../types/parameters';
import { WorkbenchType } from '../types/navigation';
export enum SubcategoryId { export enum SubcategoryId {
SIGNING = 'signing', SIGNING = 'signing',
@ -28,7 +29,6 @@ export type ToolRegistryEntry = {
icon: React.ReactNode; icon: React.ReactNode;
name: string; name: string;
component: React.ComponentType<BaseToolProps> | null; component: React.ComponentType<BaseToolProps> | null;
view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
description: string; description: string;
categoryId: ToolCategoryId; categoryId: ToolCategoryId;
subcategoryId: SubcategoryId; subcategoryId: SubcategoryId;
@ -37,6 +37,10 @@ export type ToolRegistryEntry = {
endpoints?: string[]; endpoints?: string[];
link?: string; link?: string;
type?: string; type?: string;
// URL path for routing (e.g., '/split-pdfs', '/compress-pdf')
urlPath?: string;
// Workbench type for navigation
workbench?: WorkbenchType;
// Operation configuration for automation // Operation configuration for automation
operationConfig?: ToolOperationConfig<any>; operationConfig?: ToolOperationConfig<any>;
// Settings component for automation configuration // Settings component for automation configuration
@ -107,3 +111,30 @@ export const getAllApplicationEndpoints = (
const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : [];
return Array.from(new Set([...toolEp, ...convEp])); return Array.from(new Set([...toolEp, ...convEp]));
}; };
/**
* Default workbench for tools that don't specify one
* Returns null to trigger the default case in Workbench component (ToolRenderer)
*/
export const getDefaultToolWorkbench = (): WorkbenchType => 'fileEditor';
/**
* Get workbench type for a tool
*/
export const getToolWorkbench = (tool: ToolRegistryEntry): WorkbenchType => {
return tool.workbench || getDefaultToolWorkbench();
};
/**
* Get URL path for a tool
*/
export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string => {
return tool.urlPath || `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
};
/**
* Check if a tool ID exists in the registry
*/
export const isValidToolId = (toolId: string, registry: ToolRegistry): boolean => {
return toolId in registry;
};

View File

@ -1,66 +1,127 @@
import React, { useMemo } from 'react'; import React, { useMemo } from "react";
import LocalIcon from '../components/shared/LocalIcon'; import LocalIcon from "../components/shared/LocalIcon";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import SplitPdfPanel from "../tools/Split"; import SplitPdfPanel from "../tools/Split";
import CompressPdfPanel from "../tools/Compress"; import CompressPdfPanel from "../tools/Compress";
import OCRPanel from '../tools/OCR'; import OCRPanel from "../tools/OCR";
import ConvertPanel from '../tools/Convert'; import ConvertPanel from "../tools/Convert";
import Sanitize from '../tools/Sanitize'; import Sanitize from "../tools/Sanitize";
import AddPassword from '../tools/AddPassword'; import AddPassword from "../tools/AddPassword";
import ChangePermissions from '../tools/ChangePermissions'; import ChangePermissions from "../tools/ChangePermissions";
import RemovePassword from '../tools/RemovePassword'; import RemovePassword from "../tools/RemovePassword";
import { SubcategoryId, ToolCategoryId, ToolRegistry } from './toolsTaxonomy'; import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
import AddWatermark from '../tools/AddWatermark'; import AddWatermark from "../tools/AddWatermark";
import Repair from '../tools/Repair'; import Repair from "../tools/Repair";
import SingleLargePage from '../tools/SingleLargePage'; import SingleLargePage from "../tools/SingleLargePage";
import UnlockPdfForms from '../tools/UnlockPdfForms'; import UnlockPdfForms from "../tools/UnlockPdfForms";
import RemoveCertificateSign from '../tools/RemoveCertificateSign'; import RemoveCertificateSign from "../tools/RemoveCertificateSign";
import { compressOperationConfig } from '../hooks/tools/compress/useCompressOperation'; import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
import { splitOperationConfig } from '../hooks/tools/split/useSplitOperation'; import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
import { addPasswordOperationConfig } from '../hooks/tools/addPassword/useAddPasswordOperation'; import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
import { removePasswordOperationConfig } from '../hooks/tools/removePassword/useRemovePasswordOperation'; import { removePasswordOperationConfig } from "../hooks/tools/removePassword/useRemovePasswordOperation";
import { sanitizeOperationConfig } from '../hooks/tools/sanitize/useSanitizeOperation'; import { sanitizeOperationConfig } from "../hooks/tools/sanitize/useSanitizeOperation";
import { repairOperationConfig } from '../hooks/tools/repair/useRepairOperation'; import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation";
import { addWatermarkOperationConfig } from '../hooks/tools/addWatermark/useAddWatermarkOperation'; import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation";
import { unlockPdfFormsOperationConfig } from '../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation'; import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
import { singleLargePageOperationConfig } from '../hooks/tools/singleLargePage/useSingleLargePageOperation'; import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
import { ocrOperationConfig } from '../hooks/tools/ocr/useOCROperation'; import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
import { convertOperationConfig } from '../hooks/tools/convert/useConvertOperation'; import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
import { removeCertificateSignOperationConfig } from '../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation'; import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
import { changePermissionsOperationConfig } from '../hooks/tools/changePermissions/useChangePermissionsOperation'; import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
import CompressSettings from '../components/tools/compress/CompressSettings'; import CompressSettings from "../components/tools/compress/CompressSettings";
import SplitSettings from '../components/tools/split/SplitSettings'; import SplitSettings from "../components/tools/split/SplitSettings";
import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings'; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
import RemovePasswordSettings from '../components/tools/removePassword/RemovePasswordSettings'; import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
import SanitizeSettings from '../components/tools/sanitize/SanitizeSettings'; import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
import RepairSettings from '../components/tools/repair/RepairSettings'; import RepairSettings from "../components/tools/repair/RepairSettings";
import UnlockPdfFormsSettings from '../components/tools/unlockPdfForms/UnlockPdfFormsSettings'; import UnlockPdfFormsSettings from "../components/tools/unlockPdfForms/UnlockPdfFormsSettings";
import AddWatermarkSingleStepSettings from '../components/tools/addWatermark/AddWatermarkSingleStepSettings'; import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/AddWatermarkSingleStepSettings";
import OCRSettings from '../components/tools/ocr/OCRSettings'; import OCRSettings from "../components/tools/ocr/OCRSettings";
import ConvertSettings from '../components/tools/convert/ConvertSettings'; import ConvertSettings from "../components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from '../components/tools/changePermissions/ChangePermissionsSettings'; import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
// Convert tool supported file formats // Convert tool supported file formats
export const CONVERT_SUPPORTED_FORMATS = [ export const CONVERT_SUPPORTED_FORMATS = [
// Microsoft Office // Microsoft Office
"doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx", "doc",
"docx",
"dot",
"dotx",
"csv",
"xls",
"xlsx",
"xlt",
"xltx",
"slk",
"dif",
"ppt",
"pptx",
// OpenDocument // OpenDocument
"odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg", "odt",
"ott",
"ods",
"ots",
"odp",
"otp",
"odg",
"otg",
// Text formats // Text formats
"txt", "text", "xml", "rtf", "html", "lwp", "md", "txt",
"text",
"xml",
"rtf",
"html",
"lwp",
"md",
// Images // Images
"bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp", "bmp",
"gif",
"jpeg",
"jpg",
"png",
"tif",
"tiff",
"pbm",
"pgm",
"ppm",
"ras",
"xbm",
"xpm",
"svg",
"svm",
"wmf",
"webp",
// StarOffice // StarOffice
"sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw", "sda",
"sdc",
"sdd",
"sdw",
"stc",
"std",
"sti",
"stw",
"sxd",
"sxg",
"sxi",
"sxw",
// Email formats // Email formats
"eml", "eml",
// Archive formats // Archive formats
"zip", "zip",
// Other // Other
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf" "dbf",
]; "fods",
"vsd",
"vor",
"vor3",
"vor4",
"uop",
"pct",
"ps",
"pdf",
];
// Hook to get the translated tool registry // Hook to get the translated tool registry
export function useFlatToolRegistry(): ToolRegistry { export function useFlatToolRegistry(): ToolRegistry {
@ -70,119 +131,111 @@ export function useFlatToolRegistry(): ToolRegistry {
const allTools: ToolRegistry = { const allTools: ToolRegistry = {
// Signing // Signing
"certSign": { certSign: {
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.certSign.title", "Sign with Certificate"), name: t("home.certSign.title", "Sign with Certificate"),
component: null, component: null,
view: "sign",
description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"), description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING subcategoryId: SubcategoryId.SIGNING,
}, },
"sign": { sign: {
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sign.title", "Sign"), name: t("home.sign.title", "Sign"),
component: null, component: null,
view: "sign",
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"), description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING subcategoryId: SubcategoryId.SIGNING,
}, },
// Document Security // Document Security
"addPassword": { addPassword: {
icon: <LocalIcon icon="password-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="password-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addPassword.title", "Add Password"), name: t("home.addPassword.title", "Add Password"),
component: AddPassword, component: AddPassword,
view: "security",
description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"), description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1, maxFiles: -1,
endpoints: ["add-password"], endpoints: ["add-password"],
operationConfig: addPasswordOperationConfig, operationConfig: addPasswordOperationConfig,
settingsComponent: AddPasswordSettings settingsComponent: AddPasswordSettings,
}, },
"watermark": { watermark: {
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.watermark.title", "Add Watermark"), name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark, component: AddWatermark,
view: "format",
maxFiles: -1, maxFiles: -1,
description: t("home.watermark.desc", "Add a custom watermark to your PDF document."), description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
endpoints: ["add-watermark"], endpoints: ["add-watermark"],
operationConfig: addWatermarkOperationConfig, operationConfig: addWatermarkOperationConfig,
settingsComponent: AddWatermarkSingleStepSettings settingsComponent: AddWatermarkSingleStepSettings,
}, },
"add-stamp": { "add-stamp": {
icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.AddStampRequest.title", "Add Stamp to PDF"), name: t("home.AddStampRequest.title", "Add Stamp to PDF"),
component: null, component: null,
view: "format",
description: t("home.AddStampRequest.desc", "Add text or add image stamps at set locations"), description: t("home.AddStampRequest.desc", "Add text or add image stamps at set locations"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
}, },
"sanitize": { sanitize: {
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sanitize.title", "Sanitize"), name: t("home.sanitize.title", "Sanitize"),
component: Sanitize, component: Sanitize,
view: "security",
maxFiles: -1, maxFiles: -1,
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"), description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
endpoints: ["sanitize-pdf"], endpoints: ["sanitize-pdf"],
operationConfig: sanitizeOperationConfig, operationConfig: sanitizeOperationConfig,
settingsComponent: SanitizeSettings settingsComponent: SanitizeSettings,
}, },
"flatten": { flatten: {
icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.flatten.title", "Flatten"), name: t("home.flatten.title", "Flatten"),
component: null, component: null,
view: "format",
description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"), description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
}, },
"unlock-pdf-forms": { "unlock-pdf-forms": {
icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"), name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
component: UnlockPdfForms, component: UnlockPdfForms,
view: "security",
description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."), description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1, maxFiles: -1,
endpoints: ["unlock-pdf-forms"], endpoints: ["unlock-pdf-forms"],
operationConfig: unlockPdfFormsOperationConfig, operationConfig: unlockPdfFormsOperationConfig,
settingsComponent: UnlockPdfFormsSettings settingsComponent: UnlockPdfFormsSettings,
}, },
"manage-certificates": { "manage-certificates": {
icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.manageCertificates.title", "Manage Certificates"), name: t("home.manageCertificates.title", "Manage Certificates"),
component: null, component: null,
view: "security", description: t(
description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."), "home.manageCertificates.desc",
"Import, export, or delete digital certificate files used for signing PDFs."
),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
}, },
"change-permissions": { "change-permissions": {
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
name: t("home.changePermissions.title", "Change Permissions"), name: t("home.changePermissions.title", "Change Permissions"),
component: ChangePermissions, component: ChangePermissions,
view: "security",
description: t("home.changePermissions.desc", "Change document restrictions and permissions"), description: t("home.changePermissions.desc", "Change document restrictions and permissions"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1, maxFiles: -1,
endpoints: ["add-password"], endpoints: ["add-password"],
operationConfig: changePermissionsOperationConfig, operationConfig: changePermissionsOperationConfig,
settingsComponent: ChangePermissionsSettings settingsComponent: ChangePermissionsSettings,
}, },
// Verification // Verification
@ -190,422 +243,393 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"), name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
component: null, component: null,
view: "extract",
description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"), description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION subcategoryId: SubcategoryId.VERIFICATION,
}, },
"validate-pdf-signature": { "validate-pdf-signature": {
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.validateSignature.title", "Validate PDF Signature"), name: t("home.validateSignature.title", "Validate PDF Signature"),
component: null, component: null,
view: "security",
description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"), description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION subcategoryId: SubcategoryId.VERIFICATION,
}, },
// Document Review // Document Review
"read": { read: {
icon: <LocalIcon icon="article-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="article-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.read.title", "Read"), name: t("home.read.title", "Read"),
component: null, component: null,
view: "view", workbench: "viewer",
description: t("home.read.desc", "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."), description: t(
"home.read.desc",
"View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
}, },
"change-metadata": { "change-metadata": {
icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.changeMetadata.title", "Change Metadata"), name: t("home.changeMetadata.title", "Change Metadata"),
component: null, component: null,
view: "format",
description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"), description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
}, },
// Page Formatting // Page Formatting
"cropPdf": { cropPdf: {
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.crop.title", "Crop PDF"), name: t("home.crop.title", "Crop PDF"),
component: null, component: null,
view: "format",
description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"), description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
"rotate": { rotate: {
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.rotate.title", "Rotate"), name: t("home.rotate.title", "Rotate"),
component: null, component: null,
view: "format",
description: t("home.rotate.desc", "Easily rotate your PDFs."), description: t("home.rotate.desc", "Easily rotate your PDFs."),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
"splitPdf": { splitPdf: {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.split.title", "Split"), name: t("home.split.title", "Split"),
component: SplitPdfPanel, component: SplitPdfPanel,
view: "split",
description: t("home.split.desc", "Split PDFs into multiple documents"), description: t("home.split.desc", "Split PDFs into multiple documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
operationConfig: splitOperationConfig, operationConfig: splitOperationConfig,
settingsComponent: SplitSettings settingsComponent: SplitSettings,
}, },
"reorganize-pages": { "reorganize-pages": {
icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.reorganizePages.title", "Reorganize Pages"), name: t("home.reorganizePages.title", "Reorganize Pages"),
component: null, component: null,
view: "pageEditor", workbench: "pageEditor",
description: t("home.reorganizePages.desc", "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."), description: t(
"home.reorganizePages.desc",
"Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
"adjust-page-size-scale": { "adjust-page-size-scale": {
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.scalePages.title", "Adjust page size/scale"), name: t("home.scalePages.title", "Adjust page size/scale"),
component: null, component: null,
view: "format",
description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."), description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
"addPageNumbers": { addPageNumbers: {
icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addPageNumbers.title", "Add Page Numbers"), name: t("home.addPageNumbers.title", "Add Page Numbers"),
component: null, component: null,
view: "format",
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"), description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
"multi-page-layout": { "multi-page-layout": {
icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pageLayout.title", "Multi-Page Layout"), name: t("home.pageLayout.title", "Multi-Page Layout"),
component: null, component: null,
view: "format",
description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"), description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
"single-large-page": { "single-large-page": {
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"), name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
component: SingleLargePage, component: SingleLargePage,
view: "format",
description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"), description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1, maxFiles: -1,
endpoints: ["pdf-to-single-page"], endpoints: ["pdf-to-single-page"],
operationConfig: singleLargePageOperationConfig operationConfig: singleLargePageOperationConfig,
}, },
"add-attachments": { "add-attachments": {
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.attachments.title", "Add Attachments"), name: t("home.attachments.title", "Add Attachments"),
component: null, component: null,
view: "format",
description: t("home.attachments.desc", "Add or remove embedded files (attachments) to/from a PDF"), description: t("home.attachments.desc", "Add or remove embedded files (attachments) to/from a PDF"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
// Extraction // Extraction
"extractPages": { extractPages: {
icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.extractPages.title", "Extract Pages"), name: t("home.extractPages.title", "Extract Pages"),
component: null, component: null,
view: "extract",
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"), description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION subcategoryId: SubcategoryId.EXTRACTION,
}, },
"extract-images": { "extract-images": {
icon: <LocalIcon icon="filter-alt" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="filter-alt" width="1.5rem" height="1.5rem" />,
name: t("home.extractImages.title", "Extract Images"), name: t("home.extractImages.title", "Extract Images"),
component: null, component: null,
view: "extract",
description: t("home.extractImages.desc", "Extract images from PDF documents"), description: t("home.extractImages.desc", "Extract images from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION subcategoryId: SubcategoryId.EXTRACTION,
}, },
// Removal // Removal
"removePages": { removePages: {
icon: <LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removePages.title", "Remove Pages"), name: t("home.removePages.title", "Remove Pages"),
component: null, component: null,
view: "remove",
description: t("home.removePages.desc", "Remove specific pages from a PDF document"), description: t("home.removePages.desc", "Remove specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL,
}, },
"remove-blank-pages": { "remove-blank-pages": {
icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeBlanks.title", "Remove Blank Pages"), name: t("home.removeBlanks.title", "Remove Blank Pages"),
component: null, component: null,
view: "remove",
description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"), description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL,
}, },
"remove-annotations": { "remove-annotations": {
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeAnnotations.title", "Remove Annotations"), name: t("home.removeAnnotations.title", "Remove Annotations"),
component: null, component: null,
view: "remove",
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"), description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL,
}, },
"remove-image": { "remove-image": {
icon: <LocalIcon icon="remove-selection-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="remove-selection-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeImagePdf.title", "Remove Image"), name: t("home.removeImagePdf.title", "Remove Image"),
component: null, component: null,
view: "format",
description: t("home.removeImagePdf.desc", "Remove images from PDF documents"), description: t("home.removeImagePdf.desc", "Remove images from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL,
}, },
"remove-password": { "remove-password": {
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removePassword.title", "Remove Password"), name: t("home.removePassword.title", "Remove Password"),
component: RemovePassword, component: RemovePassword,
view: "security",
description: t("home.removePassword.desc", "Remove password protection from PDF documents"), description: t("home.removePassword.desc", "Remove password protection from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
endpoints: ["remove-password"], endpoints: ["remove-password"],
maxFiles: -1, maxFiles: -1,
operationConfig: removePasswordOperationConfig, operationConfig: removePasswordOperationConfig,
settingsComponent: RemovePasswordSettings settingsComponent: RemovePasswordSettings,
}, },
"remove-certificate-sign": { "remove-certificate-sign": {
icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeCertSign.title", "Remove Certificate Sign"), name: t("home.removeCertSign.title", "Remove Certificate Sign"),
component: RemoveCertificateSign, component: RemoveCertificateSign,
view: "security",
description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"), description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
maxFiles: -1, maxFiles: -1,
endpoints: ["remove-certificate-sign"], endpoints: ["remove-certificate-sign"],
operationConfig: removeCertificateSignOperationConfig operationConfig: removeCertificateSignOperationConfig,
}, },
// Automation // Automation
"automate": { automate: {
icon: <LocalIcon icon="automation-outline" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="automation-outline" width="1.5rem" height="1.5rem" />,
name: t("home.automate.title", "Automate"), name: t("home.automate.title", "Automate"),
component: React.lazy(() => import('../tools/Automate')), component: React.lazy(() => import("../tools/Automate")),
view: "format", description: t(
description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."), "home.automate.desc",
"Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION, subcategoryId: SubcategoryId.AUTOMATION,
maxFiles: -1, maxFiles: -1,
supportedFormats: CONVERT_SUPPORTED_FORMATS, supportedFormats: CONVERT_SUPPORTED_FORMATS,
endpoints: ["handleData"] endpoints: ["handleData"],
}, },
"auto-rename-pdf-file": { "auto-rename-pdf-file": {
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.auto-rename.title", "Auto Rename PDF File"), name: t("home.auto-rename.title", "Auto Rename PDF File"),
component: null, component: null,
view: "format",
description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"), description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION subcategoryId: SubcategoryId.AUTOMATION,
}, },
"auto-split-pages": { "auto-split-pages": {
icon: <LocalIcon icon="split-scene-right-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="split-scene-right-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.autoSplitPDF.title", "Auto Split Pages"), name: t("home.autoSplitPDF.title", "Auto Split Pages"),
component: null, component: null,
view: "format",
description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"), description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION subcategoryId: SubcategoryId.AUTOMATION,
}, },
"auto-split-by-size-count": { "auto-split-by-size-count": {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"), name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"),
component: null, component: null,
view: "format",
description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"), description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION subcategoryId: SubcategoryId.AUTOMATION,
}, },
// Advanced Formatting // Advanced Formatting
"adjustContrast": { adjustContrast: {
icon: <LocalIcon icon="palette" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="palette" width="1.5rem" height="1.5rem" />,
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"), name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: null, component: null,
view: "format",
description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"), description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
}, },
"repair": { repair: {
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.repair.title", "Repair"), name: t("home.repair.title", "Repair"),
component: Repair, component: Repair,
view: "format",
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"), description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
maxFiles: -1, maxFiles: -1,
endpoints: ["repair"], endpoints: ["repair"],
operationConfig: repairOperationConfig, operationConfig: repairOperationConfig,
settingsComponent: RepairSettings settingsComponent: RepairSettings,
}, },
"detect-split-scanned-photos": { "detect-split-scanned-photos": {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"), name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"),
component: null, component: null,
view: "format",
description: t("home.ScannerImageSplit.desc", "Detect and split scanned photos into separate pages"), description: t("home.ScannerImageSplit.desc", "Detect and split scanned photos into separate pages"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
}, },
"overlay-pdfs": { "overlay-pdfs": {
icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.overlay-pdfs.title", "Overlay PDFs"), name: t("home.overlay-pdfs.title", "Overlay PDFs"),
component: null, component: null,
view: "format",
description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"), description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
}, },
"replace-and-invert-color": { "replace-and-invert-color": {
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.replaceColorPdf.title", "Replace & Invert Color"), name: t("home.replaceColorPdf.title", "Replace & Invert Color"),
component: null, component: null,
view: "format",
description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"), description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
}, },
"add-image": { "add-image": {
icon: <LocalIcon icon="image-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="image-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addImage.title", "Add Image"), name: t("home.addImage.title", "Add Image"),
component: null, component: null,
view: "format",
description: t("home.addImage.desc", "Add images to PDF documents"), description: t("home.addImage.desc", "Add images to PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
}, },
"edit-table-of-contents": { "edit-table-of-contents": {
icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.editTableOfContents.title", "Edit Table of Contents"), name: t("home.editTableOfContents.title", "Edit Table of Contents"),
component: null, component: null,
view: "format",
description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"), description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
}, },
"scanner-effect": { "scanner-effect": {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.fakeScan.title", "Scanner Effect"), name: t("home.fakeScan.title", "Scanner Effect"),
component: null, component: null,
view: "format",
description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"), description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
}, },
// Developer Tools // Developer Tools
"show-javascript": { "show-javascript": {
icon: <LocalIcon icon="javascript-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="javascript-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.showJS.title", "Show JavaScript"), name: t("home.showJS.title", "Show JavaScript"),
component: null, component: null,
view: "extract",
description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"), description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
}, },
"dev-api": { "dev-api": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devApi.title", "API"), name: t("home.devApi.title", "API"),
component: null, component: null,
view: "external",
description: t("home.devApi.desc", "Link to API documentation"), description: t("home.devApi.desc", "Link to API documentation"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html" link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html",
}, },
"dev-folder-scanning": { "dev-folder-scanning": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devFolderScanning.title", "Automated Folder Scanning"), name: t("home.devFolderScanning.title", "Automated Folder Scanning"),
component: null, component: null,
view: "external",
description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"), description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/" link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
}, },
"dev-sso-guide": { "dev-sso-guide": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devSsoGuide.title", "SSO Guide"), name: t("home.devSsoGuide.title", "SSO Guide"),
component: null, component: null,
view: "external",
description: t("home.devSsoGuide.desc", "Link to SSO guide"), description: t("home.devSsoGuide.desc", "Link to SSO guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration", link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
}, },
"dev-airgapped": { "dev-airgapped": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devAirgapped.title", "Air-gapped Setup"), name: t("home.devAirgapped.title", "Air-gapped Setup"),
component: null, component: null,
view: "external",
description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"), description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Pro/#activation" link: "https://docs.stirlingpdf.com/Pro/#activation",
}, },
// Recommended Tools // Recommended Tools
"compare": { compare: {
icon: <LocalIcon icon="compare-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="compare-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.compare.title", "Compare"), name: t("home.compare.title", "Compare"),
component: null, component: null,
view: "format",
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL subcategoryId: SubcategoryId.GENERAL,
}, },
"compress": { compress: {
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.compress.title", "Compress"), name: t("home.compress.title", "Compress"),
component: CompressPdfPanel, component: CompressPdfPanel,
view: "compress",
description: t("home.compress.desc", "Compress PDFs to reduce their file size."), description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1, maxFiles: -1,
operationConfig: compressOperationConfig, operationConfig: compressOperationConfig,
settingsComponent: CompressSettings settingsComponent: CompressSettings,
}, },
"convert": { convert: {
icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.convert.title", "Convert"), name: t("home.convert.title", "Convert"),
component: ConvertPanel, component: ConvertPanel,
view: "convert",
description: t("home.convert.desc", "Convert files to and from PDF format"), description: t("home.convert.desc", "Convert files to and from PDF format"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
@ -625,52 +649,50 @@ export function useFlatToolRegistry(): ToolRegistry {
"pdf-to-csv", "pdf-to-csv",
"pdf-to-markdown", "pdf-to-markdown",
"pdf-to-pdfa", "pdf-to-pdfa",
"eml-to-pdf" "eml-to-pdf",
], ],
operationConfig: convertOperationConfig, operationConfig: convertOperationConfig,
settingsComponent: ConvertSettings settingsComponent: ConvertSettings,
}, },
"mergePdfs": { mergePdfs: {
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.merge.title", "Merge"), name: t("home.merge.title", "Merge"),
component: null, component: null,
view: "merge",
description: t("home.merge.desc", "Merge multiple PDFs into a single document"), description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1 maxFiles: -1,
}, },
"multi-tool": { "multi-tool": {
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.multiTool.title", "Multi-Tool"), name: t("home.multiTool.title", "Multi-Tool"),
component: null, component: null,
view: "pageEditor", workbench: "pageEditor",
description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1 maxFiles: -1,
}, },
"ocr": { ocr: {
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.ocr.title", "OCR"), name: t("home.ocr.title", "OCR"),
component: OCRPanel, component: OCRPanel,
view: "convert",
description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"), description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1, maxFiles: -1,
operationConfig: ocrOperationConfig, operationConfig: ocrOperationConfig,
settingsComponent: OCRSettings settingsComponent: OCRSettings,
}, },
"redact": { redact: {
icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.redact.title", "Redact"), name: t("home.redact.title", "Redact"),
component: null, component: null,
view: "redact",
description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"), description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL subcategoryId: SubcategoryId.GENERAL,
}, },
}; };
@ -678,7 +700,7 @@ export function useFlatToolRegistry(): ToolRegistry {
return allTools; return allTools;
} }
const filteredTools = Object.keys(allTools) const filteredTools = Object.keys(allTools)
.filter(key => allTools[key].component !== null || allTools[key].link) .filter((key) => allTools[key].component !== null || allTools[key].link)
.reduce((obj, key) => { .reduce((obj, key) => {
obj[key] = allTools[key]; obj[key] = allTools[key];
return obj; return obj;

View File

@ -45,16 +45,16 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
export function useSuggestedTools(): SuggestedTool[] { export function useSuggestedTools(): SuggestedTool[] {
const { actions } = useNavigationActions(); const { actions } = useNavigationActions();
const { selectedToolKey } = useNavigationState(); const { selectedTool } = useNavigationState();
return useMemo(() => { return useMemo(() => {
// Filter out the current tool // Filter out the current tool
const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedToolKey); const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedTool);
// Add navigation function to each tool // Add navigation function to each tool
return filteredTools.map(tool => ({ return filteredTools.map(tool => ({
...tool, ...tool,
navigate: () => actions.handleToolSelect(tool.id) navigate: () => actions.setSelectedTool(tool.id)
})); }));
}, [selectedToolKey, actions]); }, [selectedTool, actions]);
} }

View File

@ -1,123 +1,90 @@
/** /**
* URL synchronization hooks for tool routing * URL synchronization hooks for tool routing with registry support
*/ */
import { useEffect, useCallback } from 'react'; import { useEffect, useCallback } from 'react';
import { ModeType } from '../types/navigation'; import { WorkbenchType, ToolId, ToolRoute } from '../types/navigation';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
import { ToolRegistry } from '../data/toolsTaxonomy';
/** /**
* Hook to sync navigation mode with URL * Hook to sync workbench and tool with URL using registry
*/ */
export function useNavigationUrlSync( export function useNavigationUrlSync(
currentMode: ModeType, workbench: WorkbenchType,
setMode: (mode: ModeType) => void, selectedTool: ToolId | null,
setWorkbench: (workbench: WorkbenchType) => void,
setSelectedTool: (toolId: ToolId | null) => void,
handleToolSelect: (toolId: string) => void,
clearToolSelection: () => void,
registry: ToolRegistry,
enableSync: boolean = true enableSync: boolean = true
) { ) {
// Initialize mode from URL on mount // Initialize workbench and tool from URL on mount
useEffect(() => { useEffect(() => {
if (!enableSync) return; if (!enableSync) return;
const route = parseToolRoute(); const route = parseToolRoute(registry);
if (route.mode !== currentMode) { if (route.toolId !== selectedTool) {
setMode(route.mode); if (route.toolId) {
handleToolSelect(route.toolId);
} else {
clearToolSelection();
}
} }
}, []); // Only run on mount }, []); // Only run on mount
// Update URL when mode changes // Update URL when tool or workbench changes
useEffect(() => { useEffect(() => {
if (!enableSync) return; if (!enableSync) return;
// Only update URL for actual tool modes, not internal UI modes if (selectedTool) {
// URL clearing is handled by useToolWorkflowUrlSync when selectedToolKey becomes null updateToolRoute(selectedTool, registry);
if (currentMode !== 'fileEditor' && currentMode !== 'pageEditor' && currentMode !== 'viewer') { } else {
updateToolRoute(currentMode, currentMode); // Clear URL when no tool is selected
clearToolRoute();
} }
}, [currentMode, enableSync]); }, [selectedTool, registry, enableSync]);
// Handle browser back/forward navigation // Handle browser back/forward navigation
useEffect(() => { useEffect(() => {
if (!enableSync) return; if (!enableSync) return;
const handlePopState = () => { const handlePopState = () => {
const route = parseToolRoute(); const route = parseToolRoute(registry);
if (route.mode !== currentMode) { if (route.toolId !== selectedTool) {
setMode(route.mode); if (route.toolId) {
handleToolSelect(route.toolId);
} else {
clearToolSelection();
}
} }
}; };
window.addEventListener('popstate', handlePopState); window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState);
}, [currentMode, setMode, enableSync]); }, [selectedTool, handleToolSelect, clearToolSelection, registry, enableSync]);
} }
/** /**
* Hook to sync tool workflow with URL * Hook to programmatically navigate to tools with registry support
*/ */
export function useToolWorkflowUrlSync( export function useToolNavigation(registry: ToolRegistry) {
selectedToolKey: string | null, const navigateToTool = useCallback((toolId: ToolId) => {
selectTool: (toolKey: string) => void, updateToolRoute(toolId, registry);
clearTool: () => void,
enableSync: boolean = true
) {
// Initialize tool from URL on mount
useEffect(() => {
if (!enableSync) return;
const route = parseToolRoute();
if (route.toolKey && route.toolKey !== selectedToolKey) {
selectTool(route.toolKey);
} else if (!route.toolKey && selectedToolKey) {
clearTool();
}
}, []); // Only run on mount
// Update URL when tool changes
useEffect(() => {
if (!enableSync) return;
if (selectedToolKey) {
const route = parseToolRoute();
if (route.toolKey !== selectedToolKey) {
updateToolRoute(selectedToolKey as ModeType, selectedToolKey);
}
} else {
// Clear URL when no tool is selected - always clear regardless of current URL
clearToolRoute();
}
}, [selectedToolKey, enableSync]);
}
/**
* Hook to get current URL route information
*/
export function useCurrentRoute() {
const getCurrentRoute = useCallback(() => {
return parseToolRoute();
}, []);
return getCurrentRoute;
}
/**
* Hook to programmatically navigate to tools
*/
export function useToolNavigation() {
const navigateToTool = useCallback((toolKey: string) => {
updateToolRoute(toolKey as ModeType, toolKey);
// Dispatch a custom event to notify other components // Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', { window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey } detail: { toolId }
})); }));
}, []); }, [registry]);
const navigateToHome = useCallback(() => { const navigateToHome = useCallback(() => {
clearToolRoute(); clearToolRoute();
// Dispatch a custom event to notify other components // Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', { window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey: null } detail: { toolId: null }
})); }));
}, []); }, []);
@ -126,3 +93,14 @@ export function useToolNavigation() {
navigateToHome navigateToHome
}; };
} }
/**
* Hook to get current URL route information with registry support
*/
export function useCurrentRoute(registry: ToolRegistry) {
const getCurrentRoute = useCallback(() => {
return parseToolRoute(registry);
}, [registry]);
return getCurrentRoute;
}

View File

@ -8,7 +8,6 @@ import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import './i18n'; // Initialize i18next import './i18n'; // Initialize i18next
import { PostHogProvider } from 'posthog-js/react'; import { PostHogProvider } from 'posthog-js/react';
import { ScarfPixel } from './components/ScarfPixel';
// Compute initial color scheme // Compute initial color scheme
function getInitialScheme(): 'light' | 'dark' { function getInitialScheme(): 'light' | 'dark' {
@ -40,7 +39,6 @@ root.render(
}} }}
> >
<BrowserRouter> <BrowserRouter>
<ScarfPixel />
<App /> <App />
</BrowserRouter> </BrowserRouter>
</PostHogProvider> </PostHogProvider>

View File

@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFileContext } from "../contexts/FileContext"; import { useFileContext } from "../contexts/FileContext";
import { useFileSelection } from "../contexts/FileContext"; import { useFileSelection } from "../contexts/FileContext";
import { useNavigation } from "../contexts/NavigationContext"; import { useNavigationActions } from "../contexts/NavigationContext";
import { useToolWorkflow } from "../contexts/ToolWorkflowContext"; import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { createToolFlow } from "../components/tools/shared/createToolFlow";
@ -21,7 +21,7 @@ import { AUTOMATION_STEPS } from "../constants/automation";
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { selectedFiles } = useFileSelection(); const { selectedFiles } = useFileSelection();
const { setMode } = useNavigation(); const { actions } = useNavigationActions();
const { registerToolReset } = useToolWorkflow(); const { registerToolReset } = useToolWorkflow();
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION); const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
@ -223,7 +223,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
title: t('automate.reviewTitle', 'Automation Results'), title: t('automate.reviewTitle', 'Automation Results'),
onFileClick: (file: File) => { onFileClick: (file: File) => {
onPreviewFile?.(file); onPreviewFile?.(file);
setMode('viewer'); actions.setWorkbench('viewer');
} }
} }
}); });

View File

@ -43,7 +43,7 @@ const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolP
const handleThumbnailClick = (file: File) => { const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file); onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "removeCertificateSign"); sessionStorage.setItem("previousMode", "removeCertificateSign");
actions.setMode("viewer"); actions.setWorkbench("viewer");
}; };
const handleSettingsReset = () => { const handleSettingsReset = () => {

View File

@ -43,7 +43,7 @@ const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const handleThumbnailClick = (file: File) => { const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file); onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "repair"); sessionStorage.setItem("previousMode", "repair");
actions.setMode("viewer"); actions.setWorkbench("viewer");
}; };
const handleSettingsReset = () => { const handleSettingsReset = () => {

View File

@ -43,7 +43,7 @@ const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps)
const handleThumbnailClick = (file: File) => { const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file); onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "single-large-page"); sessionStorage.setItem("previousMode", "single-large-page");
actions.setMode("viewer"); actions.setWorkbench("viewer");
}; };
const handleSettingsReset = () => { const handleSettingsReset = () => {

View File

@ -43,7 +43,7 @@ const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
const handleThumbnailClick = (file: File) => { const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file); onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "unlockPdfForms"); sessionStorage.setItem("previousMode", "unlockPdfForms");
actions.setMode("viewer"); actions.setWorkbench("viewer");
}; };
const handleSettingsReset = () => { const handleSettingsReset = () => {

View File

@ -1,42 +1,31 @@
/** /**
* Shared navigation types to avoid circular dependencies * Navigation types for workbench and tool separation
*/ */
// Navigation mode types - complete list to match contexts // Define workbench values once as source of truth
export type ModeType = const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const;
| 'viewer'
| 'pageEditor'
| 'fileEditor'
| 'merge'
| 'split'
| 'compress'
| 'ocr'
| 'convert'
| 'sanitize'
| 'addPassword'
| 'changePermissions'
| 'addWatermark'
| 'removePassword'
| 'single-large-page'
| 'repair'
| 'unlockPdfForms'
| 'removeCertificateSign';
// Utility functions for mode handling // Workbench types - how the user interacts with content
export const isValidMode = (mode: string): mode is ModeType => { export type WorkbenchType = typeof WORKBENCH_TYPES[number];
const validModes: ModeType[] = [
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split', // Tool identity - what PDF operation we're performing (derived from registry)
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', export type ToolId = string;
'sanitize', 'addWatermark', 'removePassword', 'single-large-page',
'repair', 'unlockPdfForms', 'removeCertificateSign' // Navigation state
]; export interface NavigationState {
return validModes.includes(mode as ModeType); workbench: WorkbenchType;
selectedTool: ToolId | null;
}
export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor';
// Type guard using the same source of truth - no duplication
export const isValidWorkbench = (value: string): value is WorkbenchType => {
return WORKBENCH_TYPES.includes(value as WorkbenchType);
}; };
export const getDefaultMode = (): ModeType => 'fileEditor';
// Route parsing result // Route parsing result
export interface ToolRoute { export interface ToolRoute {
mode: ModeType; workbench: WorkbenchType;
toolKey: string | null; toolId: ToolId | null;
} }

View File

@ -2,10 +2,11 @@
* Navigation action interfaces to break circular dependencies * Navigation action interfaces to break circular dependencies
*/ */
import { ModeType } from './navigation'; import { WorkbenchType, ToolId } from './navigation';
export interface NavigationActions { export interface NavigationActions {
setMode: (mode: ModeType) => void; setWorkbench: (workbench: WorkbenchType) => void;
setSelectedTool: (toolId: ToolId | null) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void; setHasUnsavedChanges: (hasChanges: boolean) => void;
showNavigationWarning: (show: boolean) => void; showNavigationWarning: (show: boolean) => void;
requestNavigation: (navigationFn: () => void) => void; requestNavigation: (navigationFn: () => void) => void;
@ -14,7 +15,8 @@ export interface NavigationActions {
} }
export interface NavigationState { export interface NavigationState {
currentMode: ModeType; workbench: WorkbenchType;
selectedTool: ToolId | null;
hasUnsavedChanges: boolean; hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null; pendingNavigation: (() => void) | null;
showNavigationWarning: boolean; showNavigationWarning: boolean;

View File

@ -1,119 +1,64 @@
/** /**
* URL routing utilities for tool navigation * URL routing utilities for tool navigation with registry support
* Provides clean URL routing for the V2 tool system
*/ */
import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } from '../types/navigation'; import {
ToolId,
ToolRoute,
getDefaultWorkbench
} from '../types/navigation';
import { ToolRegistry, getToolWorkbench, getToolUrlPath, isValidToolId } from '../data/toolsTaxonomy';
/** /**
* Parse the current URL to extract tool routing information * Parse the current URL to extract tool routing information
*/ */
export function parseToolRoute(): ToolRoute { export function parseToolRoute(registry: ToolRegistry): ToolRoute {
const path = window.location.pathname; const path = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
// Extract tool from URL path (e.g., /split-pdf -> split) // Try to find tool by URL path
const toolMatch = path.match(/\/([a-zA-Z-]+)(?:-pdf)?$/); for (const [toolId, tool] of Object.entries(registry)) {
if (toolMatch) { const toolUrlPath = getToolUrlPath(toolId, tool);
const toolKey = toolMatch[1].toLowerCase(); if (path === toolUrlPath) {
// Map URL paths to tool keys and modes (excluding internal UI modes)
const toolMappings: Record<string, { mode: ModeType; toolKey: string }> = {
'split-pdfs': { mode: 'split', toolKey: 'split' },
'split': { mode: 'split', toolKey: 'split' },
'merge-pdfs': { mode: 'merge', toolKey: 'merge' },
'compress-pdf': { mode: 'compress', toolKey: 'compress' },
'convert': { mode: 'convert', toolKey: 'convert' },
'convert-pdf': { mode: 'convert', toolKey: 'convert' },
'file-to-pdf': { mode: 'convert', toolKey: 'convert' },
'eml-to-pdf': { mode: 'convert', toolKey: 'convert' },
'html-to-pdf': { mode: 'convert', toolKey: 'convert' },
'markdown-to-pdf': { mode: 'convert', toolKey: 'convert' },
'pdf-to-csv': { mode: 'convert', toolKey: 'convert' },
'pdf-to-img': { mode: 'convert', toolKey: 'convert' },
'pdf-to-markdown': { mode: 'convert', toolKey: 'convert' },
'pdf-to-pdfa': { mode: 'convert', toolKey: 'convert' },
'pdf-to-word': { mode: 'convert', toolKey: 'convert' },
'pdf-to-xml': { mode: 'convert', toolKey: 'convert' },
'add-password': { mode: 'addPassword', toolKey: 'addPassword' },
'change-permissions': { mode: 'changePermissions', toolKey: 'changePermissions' },
'sanitize-pdf': { mode: 'sanitize', toolKey: 'sanitize' },
'ocr': { mode: 'ocr', toolKey: 'ocr' },
'ocr-pdf': { mode: 'ocr', toolKey: 'ocr' },
'add-watermark': { mode: 'addWatermark', toolKey: 'addWatermark' },
'remove-password': { mode: 'removePassword', toolKey: 'removePassword' },
'single-large-page': { mode: 'single-large-page', toolKey: 'single-large-page' },
'repair': { mode: 'repair', toolKey: 'repair' },
'unlock-pdf-forms': { mode: 'unlockPdfForms', toolKey: 'unlockPdfForms' },
'remove-certificate-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' },
'remove-cert-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' }
};
const mapping = toolMappings[toolKey];
if (mapping) {
return { return {
mode: mapping.mode, workbench: getToolWorkbench(tool),
toolKey: mapping.toolKey toolId
}; };
} }
} }
// Check for query parameter fallback (e.g., ?tool=split) // Check for query parameter fallback (e.g., ?tool=split)
const toolParam = searchParams.get('tool'); const toolParam = searchParams.get('tool');
if (toolParam && isValidModeType(toolParam)) { if (toolParam && isValidToolId(toolParam, registry)) {
const tool = registry[toolParam];
return { return {
mode: toolParam as ModeType, workbench: getToolWorkbench(tool),
toolKey: toolParam toolId: toolParam
}; };
} }
// Default to page editor for home page // Default to fileEditor workbench for home page
return { return {
mode: getDefaultMode(), workbench: getDefaultWorkbench(),
toolKey: null toolId: null
}; };
} }
/** /**
* Update the URL to reflect the current tool selection * Update the URL to reflect the current tool selection
* Internal UI modes (viewer, fileEditor, pageEditor) don't get URLs
*/ */
export function updateToolRoute(mode: ModeType, toolKey?: string): void { export function updateToolRoute(toolId: ToolId, registry: ToolRegistry): void {
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
// Don't create URLs for internal UI modes const tool = registry[toolId];
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') { if (!tool) {
// If we're switching to an internal mode, clear any existing tool URL console.warn(`Tool ${toolId} not found in registry`);
if (currentPath !== '/') {
clearToolRoute();
}
return; return;
} }
let newPath = '/';
// Map modes to URL paths (only for actual tools) const newPath = getToolUrlPath(toolId, tool);
if (toolKey) {
const pathMappings: Record<string, string> = {
'split': '/split-pdfs',
'merge': '/merge-pdf',
'compress': '/compress-pdf',
'convert': '/convert-pdf',
'addPassword': '/add-password-pdf',
'changePermissions': '/change-permissions-pdf',
'sanitize': '/sanitize-pdf',
'ocr': '/ocr-pdf',
'addWatermark': '/watermark',
'removePassword': '/remove-password',
'single-large-page': '/single-large-page',
'repair': '/repair',
'unlockPdfForms': '/unlock-pdf-forms',
'removeCertificateSign': '/remove-certificate-sign'
};
newPath = pathMappings[toolKey] || `/${toolKey}`;
}
// Remove tool query parameter since we're using path-based routing // Remove tool query parameter since we're using path-based routing
searchParams.delete('tool'); searchParams.delete('tool');
@ -142,58 +87,25 @@ export function clearToolRoute(): void {
} }
/** /**
* Get clean tool name for display purposes * Get clean tool name for display purposes using registry
*/ */
export function getToolDisplayName(toolKey: string): string { export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): string {
const displayNames: Record<string, string> = { const tool = registry[toolId];
'split': 'Split PDF', return tool ? tool.name : toolId;
'merge': 'Merge PDF',
'compress': 'Compress PDF',
'convert': 'Convert PDF',
'addPassword': 'Add Password',
'changePermissions': 'Change Permissions',
'sanitize': 'Sanitize PDF',
'ocr': 'OCR PDF'
};
return displayNames[toolKey] || toolKey;
} }
// Note: isValidMode is now imported from types/navigation.ts
/** /**
* Generate shareable URL for current tool state * Generate shareable URL for current tool state using registry
* Only generates URLs for actual tools, not internal UI modes
*/ */
export function generateShareableUrl(mode: ModeType, toolKey?: string): string { export function generateShareableUrl(toolId: ToolId | null, registry: ToolRegistry): string {
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
// Don't generate URLs for internal UI modes if (!toolId || !registry[toolId]) {
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') {
return baseUrl; return baseUrl;
} }
if (toolKey) { const tool = registry[toolId];
const pathMappings: Record<string, string> = {
'split': '/split-pdf',
'merge': '/merge-pdf',
'compress': '/compress-pdf',
'convert': '/convert-pdf',
'addPassword': '/add-password-pdf',
'changePermissions': '/change-permissions-pdf',
'sanitize': '/sanitize-pdf',
'ocr': '/ocr-pdf',
'addWatermark': '/watermark',
'removePassword': '/remove-password',
'single-large-page': '/single-large-page',
'repair': '/repair',
'unlockPdfForms': '/unlock-pdf-forms',
'removeCertificateSign': '/remove-certificate-sign'
};
const path = pathMappings[toolKey] || `/${toolKey}`; const path = getToolUrlPath(toolId, tool);
return `${baseUrl}${path}`; return `${baseUrl}${path}`;
}
return baseUrl;
} }