Posthog, scarf and url navigation overhaul (#4318)

Added post hog project - always enabled
Added scarf pixel - Always enabled 
Reworked Url navigation 
Forward and back now works without reloading page

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
ConnorYoh 2025-08-28 15:42:33 +01:00 committed by GitHub
parent 5b20f11e20
commit a7d5c80188
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 945 additions and 882 deletions

View File

@ -32,6 +32,7 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^3.11.174",
"posthog-js": "^1.261.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.5.2", "react-i18next": "^15.5.2",
@ -1774,6 +1775,12 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@posthog/core": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.0.2.tgz",
"integrity": "sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.9", "version": "1.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
@ -3813,6 +3820,17 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/core-js": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -4614,6 +4632,12 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
"node_modules/file-selector": { "node_modules/file-selector": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
@ -7388,6 +7412,47 @@
"postcss": "^8.2.9" "postcss": "^8.2.9"
} }
}, },
"node_modules/posthog-js": {
"version": "1.261.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.0.tgz",
"integrity": "sha512-jyiXqyrCU+VlpbNNVRA6OQYAVut0XZMYNELCZH+XvTd981VqbE4jXn4XCBreo7XCL2gdPgDVxUVOuzNvEuKcmw==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@posthog/core": "1.0.2",
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.4"
},
"peerDependencies": {
"@rrweb/types": "2.0.0-alpha.17",
"rrweb-snapshot": "2.0.0-alpha.17"
},
"peerDependenciesMeta": {
"@rrweb/types": {
"optional": true
},
"rrweb-snapshot": {
"optional": true
}
}
},
"node_modules/posthog-js/node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"license": "Apache-2.0"
},
"node_modules/preact": {
"version": "10.27.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.1.tgz",
"integrity": "sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/precinct": { "node_modules/precinct": {
"version": "12.2.0", "version": "12.2.0",
"resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz",

View File

@ -28,6 +28,7 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^3.11.174",
"posthog-js": "^1.261.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.5.2", "react-i18next": "^15.5.2",

View File

@ -182,6 +182,7 @@ function getLicenseUrl(licenseType) {
const licenseUrls = { const licenseUrls = {
'MIT': 'https://opensource.org/licenses/MIT', 'MIT': 'https://opensource.org/licenses/MIT',
'MIT*': 'https://opensource.org/licenses/MIT',
'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0', 'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0', 'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause', 'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause',
@ -270,7 +271,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
// Known good licenses (no warnings needed) // Known good licenses (no warnings needed)
const goodLicenses = new Set([ const goodLicenses = new Set([
'MIT', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD', 'MIT', 'MIT*', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD',
'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0', 'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0',
'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0', 'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0',
'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE' 'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE'

View File

@ -412,9 +412,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: FileId) => { const handleMergeFromHere = useCallback((fileId: FileId) => {
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/workbench';
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

@ -11,7 +11,7 @@ import {
Modal Modal
} from '@mantine/core'; } from '@mantine/core';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistry } from '../../../data/toolsTaxonomy';
import ToolConfigurationModal from './ToolConfigurationModal'; import ToolConfigurationModal from './ToolConfigurationModal';
import ToolList from './ToolList'; import ToolList from './ToolList';
import IconSelector from './IconSelector'; import IconSelector from './IconSelector';
@ -24,7 +24,7 @@ interface AutomationCreationProps {
existingAutomation?: AutomationConfig; existingAutomation?: AutomationConfig;
onBack: () => void; onBack: () => void;
onComplete: (automation: AutomationConfig) => void; onComplete: (automation: AutomationConfig) => void;
toolRegistry: Record<string, ToolRegistryEntry>; toolRegistry: ToolRegistry;
} }
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) { export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {

View File

@ -33,7 +33,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
React.useEffect(() => { React.useEffect(() => {
if (automation?.operations) { if (automation?.operations) {
const steps = automation.operations.map((op: any, index: number) => { const steps = automation.operations.map((op: any, index: number) => {
const tool = toolRegistry[op.operation]; const tool = toolRegistry[op.operation as keyof typeof toolRegistry];
return { return {
id: `${op.operation}-${index}`, id: `${op.operation}-${index}`,
operation: op.operation, operation: op.operation,

View File

@ -35,7 +35,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
const [isValid, setIsValid] = useState(true); const [isValid, setIsValid] = useState(true);
// Get tool info from registry // Get tool info from registry
const toolInfo = toolRegistry[tool.operation]; const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
const SettingsComponent = toolInfo?.settingsComponent; const SettingsComponent = toolInfo?.settingsComponent;
// Initialize parameters from tool (which should contain defaults from registry) // Initialize parameters from tool (which should contain defaults from registry)

View File

@ -1,6 +1,7 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react'; import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { useNavigationUrlSync } from '../hooks/useUrlSync'; import { WorkbenchType, getDefaultWorkbench } from '../types/workbench';
import { ModeType, isValidMode, getDefaultMode } from '../types/navigation'; import { ToolId, isValidToolId } from '../types/toolId';
import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry';
/** /**
* NavigationContext - Complete navigation management system * NavigationContext - Complete navigation management system
@ -11,27 +12,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 +54,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 +105,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,75 +129,67 @@ 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() } });
}, []), }, []),
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() } });
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 = isValidToolId(toolId)? toolRegistry[toolId] : null;
const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench();
// Validate toolId and convert to ToolId type
const validToolId = isValidToolId(toolId) ? toolId : null;
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, 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}>
@ -228,13 +239,3 @@ export const useNavigationGuard = () => {
setShowNavigationWarning: actions.showNavigationWarning setShowNavigationWarning: actions.showNavigationWarning
}; };
}; };
// 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
// - URL parsing utilities
// - Route definitions
// - Navigation hooks with URL sync
// - History management
// - Breadcrumb restoration from URL params

View File

@ -6,9 +6,11 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react'; import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
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, ToolRegistry } from '../data/toolsTaxonomy';
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
import { useNavigationActions, useNavigationState } from './NavigationContext'; import { useNavigationActions, useNavigationState } from './NavigationContext';
import { ToolId, isValidToolId } from '../types/toolId';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { getDefaultWorkbench } from '../types/workbench';
// State interface // State interface
interface ToolWorkflowState { interface ToolWorkflowState {
@ -83,7 +85,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
setSearchQuery: (query: string) => void; setSearchQuery: (query: string) => void;
// Tool Actions // Tool Actions
selectTool: (toolId: string) => void; selectTool: (toolId: ToolId | null) => void;
clearToolSelection: () => void; clearToolSelection: () => void;
// Tool Reset Actions // Tool Reset Actions
@ -124,7 +126,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 +144,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 +174,17 @@ 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
const validToolId = isValidToolId(toolId) ? toolId : null;
actions.setSelectedTool(validToolId);
// Get the tool from registry to determine workbench
const tool = getSelectedTool(toolId);
if (tool && tool.workbench) {
actions.setWorkbench(tool.workbench);
} else {
actions.setWorkbench(getDefaultWorkbench());
}
// Clear search query when selecting a tool // Clear search query when selecting a tool
setSearchQuery(''); setSearchQuery('');
@ -189,13 +201,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 +226,20 @@ 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.selectedTool,
handleToolSelect,
handleBackToTools,
toolRegistry as 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

@ -1,8 +1,9 @@
import { type TFunction } from 'i18next'; import { type TFunction } from 'i18next';
import React from 'react'; import React from 'react';
import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation'; import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool'; import { BaseToolProps } from '../types/tool';
import { BaseParameters } from '../types/parameters'; import { WorkbenchType } from '../types/workbench';
import { ToolId } from '../types/toolId';
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,13 +37,17 @@ 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
settingsComponent?: React.ComponentType<any>; settingsComponent?: React.ComponentType<any>;
} }
export type ToolRegistry = Record<string /* FIX ME: Should be ToolId */, ToolRegistryEntry>; export type ToolRegistry = Record<ToolId, ToolRegistryEntry>;
export const SUBCATEGORY_ORDER: SubcategoryId[] = [ export const SUBCATEGORY_ORDER: SubcategoryId[] = [
SubcategoryId.SIGNING, SubcategoryId.SIGNING,
@ -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,128 @@
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";
import { ToolId } from "../types/toolId";
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 +132,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": { addWatermark: {
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 +244,390 @@ 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": { split: {
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": { "extract-page": {
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": { "adjust-contrast": {
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 +647,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,9 +698,9 @@ 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 as ToolId].component !== null || allTools[key as ToolId].link)
.reduce((obj, key) => { .reduce((obj, key) => {
obj[key] = allTools[key]; obj[key as ToolId] = allTools[key as ToolId];
return obj; return obj;
}, {} as ToolRegistry); }, {} as ToolRegistry);
return filteredTools; return filteredTools;

View File

@ -2,12 +2,12 @@ import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation'; import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
import { AUTOMATION_CONSTANTS } from '../../../constants/automation'; import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistry } from '../../../data/toolsTaxonomy';
interface UseAutomationFormProps { interface UseAutomationFormProps {
mode: AutomationMode; mode: AutomationMode;
existingAutomation?: AutomationConfig; existingAutomation?: AutomationConfig;
toolRegistry: Record<string, ToolRegistryEntry>; toolRegistry: ToolRegistry;
} }
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) { export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
@ -19,12 +19,12 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]); const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const getToolName = useCallback((operation: string) => { const getToolName = useCallback((operation: string) => {
const tool = toolRegistry?.[operation] as any; const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
return tool?.name || t(`tools.${operation}.name`, operation); return tool?.name || t(`tools.${operation}.name`, operation);
}, [toolRegistry, t]); }, [toolRegistry, t]);
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => { const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
const config = toolRegistry[operation]?.operationConfig; const config = toolRegistry[operation as keyof ToolRegistry]?.operationConfig;
if (config?.defaultParameters) { if (config?.defaultParameters) {
return { ...config.defaultParameters }; return { ...config.defaultParameters };
} }

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext'; import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext';
import { ToolId } from '../types/toolId';
// Material UI Icons // Material UI Icons
import CompressIcon from '@mui/icons-material/Compress'; import CompressIcon from '@mui/icons-material/Compress';
@ -9,7 +10,7 @@ import CropIcon from '@mui/icons-material/Crop';
import TextFieldsIcon from '@mui/icons-material/TextFields'; import TextFieldsIcon from '@mui/icons-material/TextFields';
export interface SuggestedTool { export interface SuggestedTool {
id: string /* FIX ME: Should be ToolId */; id: ToolId;
title: string; title: string;
icon: React.ComponentType<any>; icon: React.ComponentType<any>;
navigate: () => void; navigate: () => void;
@ -32,7 +33,7 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
icon: CleaningServicesIcon icon: CleaningServicesIcon
}, },
{ {
id: 'splitPdf', id: 'split',
title: 'Split', title: 'Split',
icon: CropIcon icon: CropIcon
}, },
@ -45,16 +46,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

@ -35,7 +35,7 @@ export const useToolManagement = (): ToolManagementResult => {
const isToolAvailable = useCallback((toolKey: string): boolean => { const isToolAvailable = useCallback((toolKey: string): boolean => {
if (endpointsLoading) return true; if (endpointsLoading) return true;
const endpoints = baseRegistry[toolKey]?.endpoints || []; const endpoints = baseRegistry[toolKey as keyof typeof baseRegistry]?.endpoints || [];
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true); return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
}, [endpointsLoading, endpointStatus, baseRegistry]); }, [endpointsLoading, endpointStatus, baseRegistry]);

View File

@ -1,129 +0,0 @@
// src/hooks/useToolUrlRouting.ts
// Focused hook for URL <-> tool-key mapping and browser history sync.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
export interface UseToolUrlRoutingOpts {
/** Currently selected tool key (from context). */
selectedToolKey: string | null;
/** Registry of available tools (key -> tool metadata). */
toolRegistry: Record<string, any> | null | undefined;
/** Select a tool (no extra side-effects). */
selectTool: (toolKey: string) => void;
/** Clear selection. */
clearToolSelection: () => void;
/** Called once during initialization if URL contains a tool; may trigger UI changes. */
onInitSelect?: (toolKey: string) => void;
/** Called when navigating via back/forward (popstate). Defaults to selectTool. */
onPopStateSelect?: (toolKey: string) => void;
/** Optional base path if the app isn't served at "/" (no trailing slash). Default: "" (root). */
basePath?: string;
}
export function useToolUrlRouting(opts: UseToolUrlRoutingOpts) {
const {
selectedToolKey,
toolRegistry,
selectTool,
clearToolSelection,
onInitSelect,
onPopStateSelect,
basePath = '',
} = opts;
// Central slug map; keep here to co-locate routing policy.
const urlMap = useMemo(
() =>
new Map<string, string>([
['compress', 'compress-pdf'],
['split', 'split-pdf'],
['convert', 'convert-pdf'],
['ocr', 'ocr-pdf'],
['merge', 'merge-pdf'],
['rotate', 'rotate-pdf'],
]),
[]
);
const getToolUrlSlug = useCallback(
(toolKey: string) => urlMap.get(toolKey) ?? toolKey,
[urlMap]
);
const getToolKeyFromSlug = useCallback(
(slug: string) => {
for (const [key, value] of urlMap) {
if (value === slug) return key;
}
return slug; // fall back to raw key
},
[urlMap]
);
// Internal flag to avoid clearing URL on initial mount.
const [hasInitialized, setHasInitialized] = useState(false);
// Normalize a pathname by stripping basePath and leading slash.
const normalizePath = useCallback(
(fullPath: string) => {
let p = fullPath;
if (basePath && p.startsWith(basePath)) {
p = p.slice(basePath.length);
}
if (p.startsWith('/')) p = p.slice(1);
return p;
},
[basePath]
);
// Update URL when tool changes (but not on first paint before any selection happens).
useEffect(() => {
if (selectedToolKey) {
const slug = getToolUrlSlug(selectedToolKey);
const newUrl = `${basePath}/${slug}`.replace(/\/+/, '/');
window.history.replaceState({}, '', newUrl);
setHasInitialized(true);
} else if (hasInitialized) {
const rootUrl = basePath || '/';
window.history.replaceState({}, '', rootUrl);
}
}, [selectedToolKey, getToolUrlSlug, hasInitialized, basePath]);
// Initialize from URL when the registry is ready and nothing is selected yet.
useEffect(() => {
if (!toolRegistry || Object.keys(toolRegistry).length === 0) return;
if (selectedToolKey) return; // don't override explicit selection
const currentPath = normalizePath(window.location.pathname);
if (currentPath) {
const toolKey = getToolKeyFromSlug(currentPath);
if (toolRegistry[toolKey]) {
(onInitSelect ?? selectTool)(toolKey);
}
}
}, [toolRegistry, selectedToolKey, getToolKeyFromSlug, selectTool, onInitSelect, normalizePath]);
// Handle browser back/forward. NOTE: useRef needs an initial value in TS.
const popHandlerRef = useRef<((this: Window, ev: PopStateEvent) => any) | null>(null);
useEffect(() => {
popHandlerRef.current = () => {
const path = normalizePath(window.location.pathname);
if (path) {
const toolKey = getToolKeyFromSlug(path);
if (toolRegistry && toolRegistry[toolKey]) {
(onPopStateSelect ?? selectTool)(toolKey);
return;
}
}
clearToolSelection();
};
const handler = (e: PopStateEvent) => popHandlerRef.current?.call(window, e);
window.addEventListener('popstate', handler);
return () => window.removeEventListener('popstate', handler);
}, [toolRegistry, selectTool, clearToolSelection, getToolKeyFromSlug, onPopStateSelect, normalizePath]);
// Expose pure helpers if you want them elsewhere (optional).
return { getToolUrlSlug, getToolKeyFromSlug };
}

View File

@ -1,120 +1,107 @@
/** /**
* URL synchronization hooks for tool routing * URL synchronization hooks for tool routing with registry support
*/ */
import { useEffect, useCallback } from 'react'; import { useEffect, useCallback, useRef } from 'react';
import { ModeType } from '../types/navigation'; import { ToolId } from '../types/toolId';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
import { ToolRegistry } from '../data/toolsTaxonomy';
import { firePixel } from '../utils/scarfTracking';
/** /**
* 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, selectedTool: ToolId | null,
setMode: (mode: ModeType) => void, handleToolSelect: (toolId: string) => void,
clearToolSelection: () => void,
registry: ToolRegistry,
enableSync: boolean = true enableSync: boolean = true
) { ) {
// Initialize mode from URL on mount const hasInitialized = useRef(false);
const prevSelectedTool = useRef<ToolId | null>(null);
// Initialize workbench and tool from URL on mount
useEffect(() => { useEffect(() => {
if (!enableSync) return; if (!enableSync) return;
const route = parseToolRoute(); // Fire pixel for initial page load
if (route.mode !== currentMode) { const currentPath = window.location.pathname;
setMode(route.mode); firePixel(currentPath);
const route = parseToolRoute(registry);
if (route.toolId !== selectedTool) {
if (route.toolId) {
handleToolSelect(route.toolId);
} else if (selectedTool !== null) {
// Only clear selection if we actually had a tool selected
// Don't clear on initial load when selectedTool starts as null
clearToolSelection();
} }
}
hasInitialized.current = true;
}, []); // 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;
if (currentMode === 'pageEditor') { if (selectedTool) {
clearToolRoute(); updateToolRoute(selectedTool, registry, false); // Use pushState for user navigation
} else { } else if (prevSelectedTool.current !== null) {
updateToolRoute(currentMode, currentMode); // Only clear URL if we had a tool before (user navigated away)
// Don't clear on initial load when both current and previous are null
if (window.location.pathname !== '/') {
clearToolRoute(false); // Use pushState for user navigation
} }
}, [currentMode, enableSync]); }
prevSelectedTool.current = selectedTool;
}, [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); // Fire pixel for back/forward navigation
const currentPath = window.location.pathname;
firePixel(currentPath);
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);
}
}
}, [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 }
})); }));
}, []); }, []);
@ -123,3 +110,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

@ -1,4 +1,5 @@
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
import '../vite-env.d.ts';
import './index.css'; // Import Tailwind CSS import './index.css'; // Import Tailwind CSS
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
@ -6,6 +7,7 @@ import { ColorSchemeScript } from '@mantine/core';
import { BrowserRouter } from 'react-router-dom'; 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';
// Compute initial color scheme // Compute initial color scheme
function getInitialScheme(): 'light' | 'dark' { function getInitialScheme(): 'light' | 'dark' {
@ -27,9 +29,18 @@ const root = ReactDOM.createRoot(container); // Finds the root DOM element
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ColorSchemeScript defaultColorScheme={getInitialScheme()} /> <ColorSchemeScript defaultColorScheme={getInitialScheme()} />
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2025-05-24',
capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
debug: import.meta.env.MODE === 'development',
}}
>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</PostHogProvider>
</React.StrictMode> </React.StrictMode>
); );

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);
@ -161,7 +161,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const filesPlaceholder = useMemo(() => { const filesPlaceholder = useMemo(() => {
if (currentStep === AUTOMATION_STEPS.RUN && stepData.automation?.operations?.length) { if (currentStep === AUTOMATION_STEPS.RUN && stepData.automation?.operations?.length) {
const firstOperation = stepData.automation.operations[0]; const firstOperation = stepData.automation.operations[0];
const toolConfig = toolRegistry[firstOperation.operation]; const toolConfig = toolRegistry[firstOperation.operation as keyof typeof toolRegistry];
// Check if the tool has supportedFormats that include non-PDF formats // Check if the tool has supportedFormats that include non-PDF formats
if (toolConfig?.supportedFormats && toolConfig.supportedFormats.length > 1) { if (toolConfig?.supportedFormats && toolConfig.supportedFormats.length > 1) {
@ -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

@ -1,42 +1,19 @@
/** /**
* Shared navigation types to avoid circular dependencies * Navigation types for workbench and tool separation
*/ */
// Navigation mode types - complete list to match contexts import { WorkbenchType } from './workbench';
export type ModeType = import { ToolId } from './toolId';
| 'viewer'
| 'pageEditor'
| 'fileEditor'
| 'merge'
| 'split'
| 'compress'
| 'ocr'
| 'convert'
| 'sanitize'
| 'addPassword'
| 'changePermissions'
| 'addWatermark'
| 'removePassword'
| 'single-large-page'
| 'repair'
| 'unlockPdfForms'
| 'removeCertificateSign';
// Utility functions for mode handling // Navigation state
export const isValidMode = (mode: string): mode is ModeType => { export interface NavigationState {
const validModes: ModeType[] = [ workbench: WorkbenchType;
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split', selectedTool: ToolId | null;
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', }
'sanitize', 'addWatermark', 'removePassword', 'single-large-page',
'repair', 'unlockPdfForms', 'removeCertificateSign'
];
return validModes.includes(mode as ModeType);
};
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,12 @@
* Navigation action interfaces to break circular dependencies * Navigation action interfaces to break circular dependencies
*/ */
import { ModeType } from './navigation'; import { WorkbenchType } from './workbench';
import { ToolId } from './toolId';
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 +16,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

@ -0,0 +1,25 @@
// Define all possible tool IDs as source of truth
const TOOL_IDS = [
'certSign', 'sign', 'addPassword', 'remove-password', 'removePages', 'remove-blank-pages', 'remove-annotations', 'remove-image',
'change-permissions', 'addWatermark',
'sanitize', 'auto-split-pages', 'auto-split-by-size-count', 'split', 'mergePdfs',
'convert', 'ocr', 'add-image', 'rotate',
'detect-split-scanned-photos',
'edit-table-of-contents',
'scanner-effect',
'auto-rename-pdf-file', 'multi-page-layout', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool',
'repair', 'compare', 'addPageNumbers', 'redact',
'flatten', 'remove-certificate-sign',
'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images',
'add-stamp', 'add-attachments', 'change-metadata', 'overlay-pdfs',
'manage-certificates', 'get-all-info-on-pdf', 'validate-pdf-signature', 'read', 'automate', 'replace-and-invert-color',
'show-javascript', 'dev-api', 'dev-folder-scanning', 'dev-sso-guide', 'dev-airgapped'
] as const;
// Tool identity - what PDF operation we're performing (type-safe)
export type ToolId = typeof TOOL_IDS[number];
// Type guard using the same source of truth
export const isValidToolId = (value: string): value is ToolId => {
return TOOL_IDS.includes(value as ToolId);
};

View File

@ -0,0 +1,12 @@
// Define workbench values once as source of truth
const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const;
// Workbench types - how the user interacts with content
export type WorkbenchType = typeof WORKBENCH_TYPES[number];
export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor';
// Type guard using the same source of truth
export const isValidWorkbench = (value: string): value is WorkbenchType => {
return WORKBENCH_TYPES.includes(value as WorkbenchType);
};

View File

@ -31,7 +31,7 @@ export const executeToolOperationWithPrefix = async (
): Promise<File[]> => { ): Promise<File[]> => {
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length }); console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
const config = toolRegistry[operationName]?.operationConfig; const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
if (!config) { if (!config) {
console.error(`❌ Tool operation not supported: ${operationName}`); console.error(`❌ Tool operation not supported: ${operationName}`);
throw new Error(`Tool operation not supported: ${operationName}`); throw new Error(`Tool operation not supported: ${operationName}`);

View File

@ -0,0 +1,28 @@
let lastFiredPathname: string | null = null;
let lastFiredTime = 0;
/**
* Fire scarf pixel for analytics tracking
* Only fires if pathname is different from last call or enough time has passed
*/
export function firePixel(pathname: string): void {
const now = Date.now();
// Only fire if pathname changed or it's been at least 1 second since last fire
if (pathname === lastFiredPathname && now - lastFiredTime < 250) {
return;
}
lastFiredPathname = pathname;
lastFiredTime = now;
const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7'
+ '&path=' + encodeURIComponent(pathname)
const img = new Image();
img.referrerPolicy = "no-referrer-when-downgrade";
img.src = url;
console.log("ScarfPixel: Fire to... " + pathname);
}

View File

@ -0,0 +1,33 @@
import { ToolId } from '../types/toolId';
// Map URL paths to tool keys (multiple URLs can map to same tool)
export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
'/split-pdfs': 'split',
'/split': 'split',
'/merge-pdfs': 'mergePdfs',
'/compress-pdf': 'compress',
'/convert': 'convert',
'/convert-pdf': 'convert',
'/file-to-pdf': 'convert',
'/eml-to-pdf': 'convert',
'/html-to-pdf': 'convert',
'/markdown-to-pdf': 'convert',
'/pdf-to-csv': 'convert',
'/pdf-to-img': 'convert',
'/pdf-to-markdown': 'convert',
'/pdf-to-pdfa': 'convert',
'/pdf-to-word': 'convert',
'/pdf-to-xml': 'convert',
'/add-password': 'addPassword',
'/change-permissions': 'change-permissions',
'/sanitize-pdf': 'sanitize',
'/ocr': 'ocr',
'/ocr-pdf': 'ocr',
'/add-watermark': 'addWatermark',
'/remove-password': 'remove-password',
'/single-large-page': 'single-large-page',
'/repair': 'repair',
'/unlock-pdf-forms': 'unlock-pdf-forms',
'/remove-certificate-sign': 'remove-certificate-sign',
'/remove-cert-sign': 'remove-certificate-sign'
};

View File

@ -1,167 +1,127 @@
/** /**
* 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 { ToolRoute } from '../types/navigation';
import { ToolId, isValidToolId } from '../types/toolId';
import { getDefaultWorkbench } from '../types/workbench';
import { ToolRegistry, getToolWorkbench, getToolUrlPath } from '../data/toolsTaxonomy';
import { firePixel } from './scarfTracking';
import { URL_TO_TOOL_MAP } from './urlMapping';
/** /**
* 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) // First, check URL mapping for multiple URL aliases
const toolMatch = path.match(/\/([a-zA-Z-]+)(?:-pdf)?$/); const mappedToolId = URL_TO_TOOL_MAP[path];
if (toolMatch) { if (mappedToolId && registry[mappedToolId]) {
const toolKey = toolMatch[1].toLowerCase(); const tool = registry[mappedToolId];
// Map URL paths to tool keys and modes (excluding internal UI modes)
const toolMappings: Record<string, { mode: ModeType; toolKey: string }> = {
'split': { mode: 'split', toolKey: 'split' },
'merge': { mode: 'merge', toolKey: 'merge' },
'compress': { mode: 'compress', toolKey: 'compress' },
'convert': { mode: 'convert', toolKey: 'convert' },
'add-password': { mode: 'addPassword', toolKey: 'addPassword' },
'change-permissions': { mode: 'changePermissions', toolKey: 'changePermissions' },
'sanitize': { mode: 'sanitize', toolKey: 'sanitize' },
'ocr': { mode: 'ocr', toolKey: 'ocr' }
};
const mapping = toolMappings[toolKey];
if (mapping) {
return { return {
mode: mapping.mode, workbench: getToolWorkbench(tool),
toolKey: mapping.toolKey toolId: mappedToolId
};
}
// Fallback: Try to find tool by primary URL path in registry
for (const [toolId, tool] of Object.entries(registry)) {
const toolUrlPath = getToolUrlPath(toolId, tool);
if (path === toolUrlPath && isValidToolId(toolId)) {
return {
workbench: getToolWorkbench(tool),
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[toolParam]) {
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 URL and fire analytics pixel
* Internal UI modes (viewer, fileEditor, pageEditor) don't get URLs
*/ */
export function updateToolRoute(mode: ModeType, toolKey?: string): void { function updateUrl(newPath: string, searchParams: URLSearchParams, replace: boolean = false): void {
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search); const queryString = searchParams.toString();
const fullUrl = newPath + (queryString ? `?${queryString}` : '');
// Don't create URLs for internal UI modes // Only update URL and fire pixel if something actually changed
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') { if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) {
// If we're switching to an internal mode, clear any existing tool URL if (replace) {
if (currentPath !== '/') { window.history.replaceState(null, '', fullUrl);
clearToolRoute(); } else {
window.history.pushState(null, '', fullUrl);
} }
firePixel(newPath);
}
}
/**
* Update the URL to reflect the current tool selection
*/
export function updateToolRoute(toolId: ToolId, registry: ToolRegistry, replace: boolean = false): void {
const tool = registry[toolId];
if (!tool) {
console.warn(`Tool ${toolId} not found in registry`);
return; return;
} }
let newPath = '/'; const newPath = getToolUrlPath(toolId, tool);
const searchParams = new URLSearchParams(window.location.search);
// Map modes to URL paths (only for actual tools)
if (toolKey) {
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'
};
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');
// Construct final URL updateUrl(newPath, searchParams, replace);
const queryString = searchParams.toString();
const fullUrl = newPath + (queryString ? `?${queryString}` : '');
// Update URL without triggering page reload
if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) {
window.history.replaceState(null, '', fullUrl);
}
} }
/** /**
* Clear tool routing and return to home page * Clear tool routing and return to home page
*/ */
export function clearToolRoute(): void { export function clearToolRoute(replace: boolean = false): void {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
searchParams.delete('tool'); searchParams.delete('tool');
const queryString = searchParams.toString(); updateUrl('/', searchParams, replace);
const url = '/' + (queryString ? `?${queryString}` : '');
window.history.replaceState(null, '', url);
} }
/** /**
* 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'
};
const path = pathMappings[toolKey] || `/${toolKey}`; const path = getToolUrlPath(toolId, tool);
return `${baseUrl}${path}`; return `${baseUrl}${path}`;
}
return baseUrl;
} }

10
frontend/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PUBLIC_POSTHOG_KEY: string;
readonly VITE_PUBLIC_POSTHOG_HOST: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}