{/* Always show the target - either selected tool or search input */}
- {selectedValue && toolRegistry[selectedValue as ToolId] && !opened ? (
+ {selectedTool && !opened ? (
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
- {}} rounded={true} disableNavigation={true}>
+ {}}
+ rounded={true}
+ disableNavigation={true}
+ />
) : (
// Show search input when no tool selected OR when dropdown is opened
diff --git a/frontend/src/components/viewer/HistoryAPIBridge.tsx b/frontend/src/components/viewer/HistoryAPIBridge.tsx
index 4227ac47d..352fdee50 100644
--- a/frontend/src/components/viewer/HistoryAPIBridge.tsx
+++ b/frontend/src/components/viewer/HistoryAPIBridge.tsx
@@ -3,13 +3,7 @@ import { useHistoryCapability } from '@embedpdf/plugin-history/react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { useSignature } from '../../contexts/SignatureContext';
import { uuidV4 } from '@embedpdf/models';
-
-export interface HistoryAPI {
- undo: () => void;
- redo: () => void;
- canUndo: () => boolean;
- canRedo: () => boolean;
-}
+import type { HistoryAPI } from './viewerTypes';
export const HistoryAPIBridge = forwardRef
(function HistoryAPIBridge(_, ref) {
const { provides: historyApi } = useHistoryCapability();
@@ -42,7 +36,7 @@ export const HistoryAPIBridge = forwardRef(function HistoryAPIBridge
const currentStoredData = getImageData(annotation.id);
// Check if the annotation lacks image data but we have it stored
if (currentStoredData && (!annotation.imageSrc || annotation.imageSrc !== currentStoredData)) {
-
+
// Generate new ID to avoid React key conflicts
const newId = uuidV4();
@@ -113,4 +107,4 @@ export const HistoryAPIBridge = forwardRef(function HistoryAPIBridge
return null; // This is a bridge component with no UI
});
-HistoryAPIBridge.displayName = 'HistoryAPIBridge';
\ No newline at end of file
+HistoryAPIBridge.displayName = 'HistoryAPIBridge';
diff --git a/frontend/src/components/viewer/LocalEmbedPDF.tsx b/frontend/src/components/viewer/LocalEmbedPDF.tsx
index cc3550dd8..d9eae66c4 100644
--- a/frontend/src/components/viewer/LocalEmbedPDF.tsx
+++ b/frontend/src/components/viewer/LocalEmbedPDF.tsx
@@ -34,8 +34,9 @@ import { SpreadAPIBridge } from './SpreadAPIBridge';
import { SearchAPIBridge } from './SearchAPIBridge';
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
import { RotateAPIBridge } from './RotateAPIBridge';
-import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
-import { HistoryAPIBridge, HistoryAPI } from './HistoryAPIBridge';
+import { SignatureAPIBridge } from './SignatureAPIBridge';
+import { HistoryAPIBridge } from './HistoryAPIBridge';
+import type { SignatureAPI, HistoryAPI } from './viewerTypes';
import { ExportAPIBridge } from './ExportAPIBridge';
interface LocalEmbedPDFProps {
diff --git a/frontend/src/components/viewer/SignatureAPIBridge.tsx b/frontend/src/components/viewer/SignatureAPIBridge.tsx
index 3e01f30b7..2042c0487 100644
--- a/frontend/src/components/viewer/SignatureAPIBridge.tsx
+++ b/frontend/src/components/viewer/SignatureAPIBridge.tsx
@@ -2,17 +2,7 @@ import { useImperativeHandle, forwardRef, useEffect } from 'react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
import { useSignature } from '../../contexts/SignatureContext';
-
-export interface SignatureAPI {
- addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
- activateDrawMode: () => void;
- activateSignaturePlacementMode: () => void;
- activateDeleteMode: () => void;
- deleteAnnotation: (annotationId: string, pageIndex: number) => void;
- updateDrawSettings: (color: string, size: number) => void;
- deactivateTools: () => void;
- getPageAnnotations: (pageIndex: number) => Promise;
-}
+import type { SignatureAPI } from './viewerTypes';
export const SignatureAPIBridge = forwardRef(function SignatureAPIBridge(_, ref) {
const { provides: annotationApi } = useAnnotationCapability();
@@ -246,4 +236,4 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI
return null; // This is a bridge component with no UI
});
-SignatureAPIBridge.displayName = 'SignatureAPIBridge';
\ No newline at end of file
+SignatureAPIBridge.displayName = 'SignatureAPIBridge';
diff --git a/frontend/src/components/viewer/viewerTypes.ts b/frontend/src/components/viewer/viewerTypes.ts
new file mode 100644
index 000000000..f8149dce7
--- /dev/null
+++ b/frontend/src/components/viewer/viewerTypes.ts
@@ -0,0 +1,24 @@
+export interface SignatureAPI {
+ addImageSignature: (
+ signatureData: string,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ pageIndex: number
+ ) => void;
+ activateDrawMode: () => void;
+ activateSignaturePlacementMode: () => void;
+ activateDeleteMode: () => void;
+ deleteAnnotation: (annotationId: string, pageIndex: number) => void;
+ updateDrawSettings: (color: string, size: number) => void;
+ deactivateTools: () => void;
+ getPageAnnotations: (pageIndex: number) => Promise;
+}
+
+export interface HistoryAPI {
+ undo: () => void;
+ redo: () => void;
+ canUndo: () => boolean;
+ canRedo: () => boolean;
+}
diff --git a/frontend/src/constants/app.ts b/frontend/src/constants/app.ts
index c83cffcfd..c61302c84 100644
--- a/frontend/src/constants/app.ts
+++ b/frontend/src/constants/app.ts
@@ -1,10 +1,3 @@
-import { useAppConfig } from '../hooks/useAppConfig';
-
-// Get base URL from app config with fallback
-export const getBaseUrl = (): string => {
- const { config } = useAppConfig();
- return config?.baseUrl || 'https://stirling.com';
-};
// Base path from Vite config - build-time constant, normalized (no trailing slash)
// When no subpath, use empty string instead of '.' to avoid relative path issues
diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx
index 3d9b4849c..b29a26d25 100644
--- a/frontend/src/contexts/NavigationContext.tsx
+++ b/frontend/src/contexts/NavigationContext.tsx
@@ -1,7 +1,7 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { WorkbenchType, getDefaultWorkbench } from '../types/workbench';
import { ToolId, isValidToolId } from '../types/toolId';
-import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry';
+import { useToolRegistry } from './ToolRegistryContext';
/**
* NavigationContext - Complete navigation management system
@@ -107,7 +107,7 @@ export const NavigationProvider: React.FC<{
enableUrlSync?: boolean;
}> = ({ children }) => {
const [state, dispatch] = useReducer(navigationReducer, initialState);
- const toolRegistry = useFlatToolRegistry();
+ const { allTools: toolRegistry } = useToolRegistry();
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
const actions: NavigationContextActions = {
diff --git a/frontend/src/contexts/SignatureContext.tsx b/frontend/src/contexts/SignatureContext.tsx
index 839e8a9df..bbb165564 100644
--- a/frontend/src/contexts/SignatureContext.tsx
+++ b/frontend/src/contexts/SignatureContext.tsx
@@ -1,7 +1,6 @@
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react';
import { SignParameters } from '../hooks/tools/sign/useSignParameters';
-import { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
-import { HistoryAPI } from '../components/viewer/HistoryAPIBridge';
+import type { SignatureAPI, HistoryAPI } from '../components/viewer/viewerTypes';
// Signature state interface
interface SignatureState {
@@ -175,4 +174,4 @@ export const useSignatureMode = () => {
isSignatureModeActive: context?.isPlacementMode || false,
hasSignatureConfig: context?.signatureConfig !== null,
};
-};
\ No newline at end of file
+};
diff --git a/frontend/src/contexts/ToolRegistryContext.tsx b/frontend/src/contexts/ToolRegistryContext.tsx
new file mode 100644
index 000000000..77dcf5567
--- /dev/null
+++ b/frontend/src/contexts/ToolRegistryContext.tsx
@@ -0,0 +1,30 @@
+import { createContext, useContext } from 'react';
+
+import type {
+ ToolRegistryEntry,
+ ToolRegistry,
+ RegularToolRegistry,
+ SuperToolRegistry,
+ LinkToolRegistry,
+} from '../data/toolsTaxonomy';
+import type { ToolId } from '../types/toolId';
+
+export interface ToolRegistryCatalog {
+ regularTools: RegularToolRegistry;
+ superTools: SuperToolRegistry;
+ linkTools: LinkToolRegistry;
+ allTools: ToolRegistry;
+ getToolById: (toolId: ToolId | null) => ToolRegistryEntry | null;
+}
+
+const ToolRegistryContext = createContext(null);
+
+export const useToolRegistry = (): ToolRegistryCatalog => {
+ const context = useContext(ToolRegistryContext);
+ if (context === null) {
+ throw new Error('useToolRegistry must be used within a ToolRegistryProvider');
+ }
+ return context;
+};
+
+export default ToolRegistryContext;
diff --git a/frontend/src/contexts/ToolRegistryProvider.tsx b/frontend/src/contexts/ToolRegistryProvider.tsx
new file mode 100644
index 000000000..30a020501
--- /dev/null
+++ b/frontend/src/contexts/ToolRegistryProvider.tsx
@@ -0,0 +1,44 @@
+import React, { useMemo } from 'react';
+
+import type { ToolId } from '../types/toolId';
+import type { ToolRegistry } from '../data/toolsTaxonomy';
+import ToolRegistryContext, { ToolRegistryCatalog } from './ToolRegistryContext';
+import { useTranslatedToolCatalog } from '../data/useTranslatedToolRegistry';
+
+interface ToolRegistryProviderProps {
+ children: React.ReactNode;
+}
+
+export const ToolRegistryProvider: React.FC = ({ children }) => {
+ const catalog = useTranslatedToolCatalog();
+
+ const contextValue = useMemo(() => {
+ const { regularTools, superTools, linkTools } = catalog;
+ const allTools: ToolRegistry = {
+ ...regularTools,
+ ...superTools,
+ ...linkTools,
+ };
+
+ const getToolById = (toolId: ToolId | null) => {
+ if (!toolId) {
+ return null;
+ }
+ return allTools[toolId] ?? null;
+ };
+
+ return {
+ regularTools,
+ superTools,
+ linkTools,
+ allTools,
+ getToolById,
+ };
+ }, [catalog]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx
index 1562e9e49..57e38b92d 100644
--- a/frontend/src/contexts/ToolWorkflowContext.tsx
+++ b/frontend/src/contexts/ToolWorkflowContext.tsx
@@ -20,6 +20,7 @@ import {
} from './toolWorkflow/toolWorkflowState';
import type { ToolPanelMode } from '../constants/toolPanel';
import { usePreferences } from './PreferencesContext';
+import { useToolRegistry } from './ToolRegistryContext';
// State interface
// Types and reducer/state moved to './toolWorkflow/state'
@@ -111,10 +112,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const navigationState = useNavigationState();
// Tool management hook
- const {
- toolRegistry,
- getSelectedTool,
- } = useToolManagement();
+ const { toolRegistry, getSelectedTool } = useToolManagement();
+ const { allTools } = useToolRegistry();
// Tool history hook
const {
@@ -315,7 +314,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
const filteredTools = useMemo(() => {
if (!toolRegistry) return [];
- return filterToolRegistryByQuery(toolRegistry as ToolRegistry, state.searchQuery);
+ return filterToolRegistryByQuery(toolRegistry, state.searchQuery);
}, [toolRegistry, state.searchQuery]);
const isPanelVisible = useMemo(() =>
@@ -327,7 +326,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
navigationState.selectedTool,
handleToolSelect,
handleBackToTools,
- toolRegistry as ToolRegistry,
+ allTools,
true
);
diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts
index 789e3b23f..a0204f1ca 100644
--- a/frontend/src/data/toolsTaxonomy.ts
+++ b/frontend/src/data/toolsTaxonomy.ts
@@ -3,7 +3,7 @@ import React from 'react';
import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool';
import { WorkbenchType } from '../types/workbench';
-import { ToolId } from '../types/toolId';
+import { LinkToolId, RegularToolId, SuperToolId, ToolId, ToolKind } from '../types/toolId';
import DrawRoundedIcon from '@mui/icons-material/DrawRounded';
import SecurityRoundedIcon from '@mui/icons-material/SecurityRounded';
import VerifiedUserRoundedIcon from '@mui/icons-material/VerifiedUserRounded';
@@ -47,7 +47,7 @@ export type ToolRegistryEntry = {
supportedFormats?: string[];
endpoints?: string[];
link?: string;
- type?: string;
+ kind?: ToolKind;
// Workbench type for navigation
workbench?: WorkbenchType;
// Operation configuration for automation
@@ -60,6 +60,9 @@ export type ToolRegistryEntry = {
synonyms?: string[];
}
+export type RegularToolRegistry = Record;
+export type SuperToolRegistry = Record;
+export type LinkToolRegistry = Record;
export type ToolRegistry = Record;
export const SUBCATEGORY_ORDER: SubcategoryId[] = [
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
index 8bc0b462c..f50b5ed55 100644
--- a/frontend/src/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -13,7 +13,15 @@ import RemovePages from "../tools/RemovePages";
import ReorganizePages from "../tools/ReorganizePages";
import { reorganizePagesOperationConfig } from "../hooks/tools/reorganizePages/useReorganizePagesOperation";
import RemovePassword from "../tools/RemovePassword";
-import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
+import {
+ SubcategoryId,
+ ToolCategoryId,
+ ToolRegistry,
+ RegularToolRegistry,
+ SuperToolRegistry,
+ LinkToolRegistry,
+} from "./toolsTaxonomy";
+import { isSuperToolId, isLinkToolId } from '../types/toolId';
import AdjustContrast from "../tools/AdjustContrast";
import AdjustContrastSingleStepSettings from "../components/tools/adjustContrast/AdjustContrastSingleStepSettings";
import { adjustContrastOperationConfig } from "../hooks/tools/adjustContrast/useAdjustContrastOperation";
@@ -109,14 +117,18 @@ import RemoveBlanksSettings from "../components/tools/removeBlanks/RemoveBlanksS
import AddPageNumbersAutomationSettings from "../components/tools/addPageNumbers/AddPageNumbersAutomationSettings";
import OverlayPdfsSettings from "../components/tools/overlayPdfs/OverlayPdfsSettings";
import ValidateSignature from "../tools/ValidateSignature";
-
-const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
-
-// Convert tool supported file formats
+import Automate from "../tools/Automate";
import { CONVERT_SUPPORTED_FORMATS } from "../constants/convertSupportedFornats";
+export interface TranslatedToolCatalog {
+ allTools: ToolRegistry;
+ regularTools: RegularToolRegistry;
+ superTools: SuperToolRegistry;
+ linkTools: LinkToolRegistry;
+}
+
// Hook to get the translated tool registry
-export function useFlatToolRegistry(): ToolRegistry {
+export function useTranslatedToolCatalog(): TranslatedToolCatalog {
const { t } = useTranslation();
return useMemo(() => {
@@ -564,7 +576,7 @@ export function useFlatToolRegistry(): ToolRegistry {
automate: {
icon: ,
name: t("home.automate.title", "Automate"),
- component: React.lazy(() => import("../tools/Automate")),
+ component: Automate,
description: t(
"home.automate.desc",
"Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
@@ -829,15 +841,26 @@ export function useFlatToolRegistry(): ToolRegistry {
},
};
- if (showPlaceholderTools) {
- return allTools;
- }
- const filteredTools = Object.keys(allTools)
- .filter((key) => allTools[key as ToolId].component !== null || allTools[key as ToolId].link)
- .reduce((obj, key) => {
- obj[key as ToolId] = allTools[key as ToolId];
- return obj;
- }, {} as ToolRegistry);
- return filteredTools;
+ const regularTools = {} as RegularToolRegistry;
+ const superTools = {} as SuperToolRegistry;
+ const linkTools = {} as LinkToolRegistry;
+
+ Object.entries(allTools).forEach(([key, entry]) => {
+ const toolId = key as ToolId;
+ if (isSuperToolId(toolId)) {
+ superTools[toolId] = entry;
+ } else if (isLinkToolId(toolId)) {
+ linkTools[toolId] = entry;
+ } else {
+ regularTools[toolId] = entry;
+ }
+ });
+
+ return {
+ allTools,
+ regularTools,
+ superTools,
+ linkTools,
+ };
}, [t]); // Only re-compute when translations change
}
diff --git a/frontend/src/hooks/tools/automate/useAutomateOperation.ts b/frontend/src/hooks/tools/automate/useAutomateOperation.ts
index e051d5f1b..e7a2ea19c 100644
--- a/frontend/src/hooks/tools/automate/useAutomateOperation.ts
+++ b/frontend/src/hooks/tools/automate/useAutomateOperation.ts
@@ -1,11 +1,12 @@
import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { useCallback } from 'react';
import { executeAutomationSequence } from '../../../utils/automationExecutor';
-import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
+import { useToolRegistry } from '../../../contexts/ToolRegistryContext';
import { AutomateParameters } from '../../../types/automation';
export function useAutomateOperation() {
- const toolRegistry = useFlatToolRegistry();
+ const { allTools } = useToolRegistry();
+ const toolRegistry = allTools;
const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => {
console.log('🚀 Starting automation execution via customProcessor', { params, files });
diff --git a/frontend/src/hooks/tools/automate/useAutomationForm.ts b/frontend/src/hooks/tools/automate/useAutomationForm.ts
index 4b5dc7b5d..91df79ac9 100644
--- a/frontend/src/hooks/tools/automate/useAutomationForm.ts
+++ b/frontend/src/hooks/tools/automate/useAutomationForm.ts
@@ -9,7 +9,7 @@ import { ToolId } from 'src/types/toolId';
interface UseAutomationFormProps {
mode: AutomationMode;
existingAutomation?: AutomationConfig;
- toolRegistry: ToolRegistry;
+ toolRegistry: Partial;
}
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
@@ -21,12 +21,12 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
const [selectedTools, setSelectedTools] = useState([]);
const getToolName = useCallback((operation: string) => {
- const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
+ const tool = toolRegistry?.[operation as ToolId] as any;
return tool?.name || t(`tools.${operation}.name`, operation);
}, [toolRegistry, t]);
const getToolDefaultParameters = useCallback((operation: string): Record => {
- const config = toolRegistry[operation as keyof ToolRegistry]?.operationConfig;
+ const config = toolRegistry[operation as ToolId]?.operationConfig;
if (config?.defaultParameters) {
return { ...config.defaultParameters };
}
diff --git a/frontend/src/hooks/useBaseUrl.ts b/frontend/src/hooks/useBaseUrl.ts
new file mode 100644
index 000000000..5db03705e
--- /dev/null
+++ b/frontend/src/hooks/useBaseUrl.ts
@@ -0,0 +1,6 @@
+import { useAppConfig } from './useAppConfig';
+
+export const useBaseUrl = (): string => {
+ const { config } = useAppConfig();
+ return config?.baseUrl || 'https://demo.stirlingpdf.com';
+};
diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx
index fd5606e2e..75d4c4c7f 100644
--- a/frontend/src/hooks/useToolManagement.tsx
+++ b/frontend/src/hooks/useToolManagement.tsx
@@ -1,6 +1,5 @@
import { useState, useCallback, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
+import { useToolRegistry } from "../contexts/ToolRegistryContext";
import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "../data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { FileId } from '../types/file';
@@ -15,19 +14,19 @@ interface ToolManagementResult {
}
export const useToolManagement = (): ToolManagementResult => {
- const { t } = useTranslation();
-
const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]);
// Build endpoints list from registry entries with fallback to legacy mapping
- const baseRegistry = useFlatToolRegistry();
+ const { allTools } = useToolRegistry();
+ const baseRegistry = allTools;
const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
const isToolAvailable = useCallback((toolKey: string): boolean => {
if (endpointsLoading) return true;
- const endpoints = baseRegistry[toolKey as keyof typeof baseRegistry]?.endpoints || [];
+ const tool = baseRegistry[toolKey as ToolId];
+ const endpoints = tool?.endpoints || [];
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
}, [endpointsLoading, endpointStatus, baseRegistry]);
@@ -35,16 +34,18 @@ export const useToolManagement = (): ToolManagementResult => {
const availableToolRegistry: Partial = {};
(Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
if (isToolAvailable(toolKey)) {
- const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
- availableToolRegistry[toolKey as ToolId] = {
- ...baseTool,
- name: baseTool.name,
- description: baseTool.description,
- };
+ const baseTool = baseRegistry[toolKey];
+ if (baseTool) {
+ availableToolRegistry[toolKey] = {
+ ...baseTool,
+ name: baseTool.name,
+ description: baseTool.description,
+ };
+ }
}
});
return availableToolRegistry;
- }, [isToolAvailable, t, baseRegistry]);
+ }, [isToolAvailable, baseRegistry]);
const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => {
return toolKey ? toolRegistry[toolKey] || null : null;
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index 168a07fbb..a80325be4 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -4,7 +4,8 @@ import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
import { Group, useMantineColorScheme } from "@mantine/core";
import { useSidebarContext } from "../contexts/SidebarContext";
import { useDocumentMeta } from "../hooks/useDocumentMeta";
-import { BASE_PATH, getBaseUrl } from "../constants/app";
+import { BASE_PATH } from "../constants/app";
+import { useBaseUrl } from "../hooks/useBaseUrl";
import { useMediaQuery } from "@mantine/hooks";
import AppsIcon from '@mui/icons-material/AppsRounded';
@@ -135,7 +136,7 @@ export default function HomePage() {
}
}, [isMobile, activeMobileView, selectedTool, setLeftPanelView]);
- const baseUrl = getBaseUrl();
+ const baseUrl = useBaseUrl();
// Update document meta when tool changes
useDocumentMeta({
diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts
index 38456157a..3ebb22fda 100644
--- a/frontend/src/setupTests.ts
+++ b/frontend/src/setupTests.ts
@@ -119,5 +119,49 @@ Object.defineProperty(window, 'matchMedia', {
})),
});
+// Provide a minimal DOMMatrix implementation for pdf.js in the test environment
+if (typeof globalThis.DOMMatrix === 'undefined') {
+ class DOMMatrixStub {
+ a = 1;
+ b = 0;
+ c = 0;
+ d = 1;
+ e = 0;
+ f = 0;
+
+ constructor(init?: string | number[]) {
+ if (Array.isArray(init) && init.length === 6) {
+ [this.a, this.b, this.c, this.d, this.e, this.f] = init as [number, number, number, number, number, number];
+ }
+ }
+
+ multiplySelf(): this {
+ return this;
+ }
+
+ translateSelf(): this {
+ return this;
+ }
+
+ scaleSelf(): this {
+ return this;
+ }
+
+ rotateSelf(): this {
+ return this;
+ }
+
+ inverse(): this {
+ return this;
+ }
+ }
+
+ Object.defineProperty(globalThis, 'DOMMatrix', {
+ value: DOMMatrixStub,
+ writable: false,
+ configurable: true,
+ });
+}
+
// Set global test timeout to prevent hangs
vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 });
diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx
index 4ec6dc646..c91ad8825 100644
--- a/frontend/src/tools/Automate.tsx
+++ b/frontend/src/tools/Automate.tsx
@@ -12,7 +12,7 @@ import AutomationRun from "../components/tools/automate/AutomationRun";
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
import { BaseToolProps } from "../types/tool";
-import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
+import { useToolRegistry } from "../contexts/ToolRegistryContext";
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation";
import { AUTOMATION_STEPS } from "../constants/automation";
@@ -27,7 +27,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const [stepData, setStepData] = useState({ step: AUTOMATION_STEPS.SELECTION });
const automateOperation = useAutomateOperation();
- const toolRegistry = useFlatToolRegistry();
+ const { regularTools: toolRegistry } = useToolRegistry();
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations();
diff --git a/frontend/src/types/toolId.ts b/frontend/src/types/toolId.ts
index a839997b0..d485cd220 100644
--- a/frontend/src/types/toolId.ts
+++ b/frontend/src/types/toolId.ts
@@ -1,5 +1,6 @@
-// Define all possible tool IDs as source of truth
-export const TOOL_IDS = [
+export type ToolKind = 'regular' | 'super' | 'link';
+
+export const REGULAR_TOOL_IDS = [
'certSign',
'sign',
'addPassword',
@@ -26,7 +27,6 @@ export const TOOL_IDS = [
'adjustContrast',
'crop',
'pdfToSinglePage',
- 'multiTool',
'repair',
'compare',
'addPageNumbers',
@@ -44,21 +44,52 @@ export const TOOL_IDS = [
'overlayPdfs',
'getPdfInfo',
'validateSignature',
- 'read',
- 'automate',
'replaceColor',
'showJS',
+ 'bookletImposition',
+] as const;
+
+export const SUPER_TOOL_IDS = [
+ 'multiTool',
+ 'read',
+ 'automate',
+] as const;
+
+const LINK_TOOL_IDS = [
'devApi',
'devFolderScanning',
'devSsoGuide',
'devAirgapped',
- 'bookletImposition',
] as const;
+const TOOL_IDS = [
+ ...REGULAR_TOOL_IDS,
+ ...SUPER_TOOL_IDS,
+ ...LINK_TOOL_IDS,
+];
+
// Tool identity - what PDF operation we're performing (type-safe)
export type ToolId = typeof TOOL_IDS[number];
+export const isValidToolId = (value: string): value is ToolId =>
+ TOOL_IDS.includes(value as ToolId);
+
+export type RegularToolId = typeof REGULAR_TOOL_IDS[number];
+export const isRegularToolId = (toolId: ToolId): toolId is RegularToolId =>
+ REGULAR_TOOL_IDS.includes(toolId as RegularToolId);
+
+export type SuperToolId = typeof SUPER_TOOL_IDS[number];
+export const isSuperToolId = (toolId: ToolId): toolId is SuperToolId =>
+ SUPER_TOOL_IDS.includes(toolId as SuperToolId);
+
+export type LinkToolId = typeof LINK_TOOL_IDS[number];
+export const isLinkToolId = (toolId: ToolId): toolId is LinkToolId =>
+ LINK_TOOL_IDS.includes(toolId as LinkToolId);
+
+
+type Assert = A;
+type Disjoint = [A & B] extends [never] ? true : false;
+
+type _Check1 = Assert>;
+type _Check2 = Assert>;
+type _Check3 = Assert>;
-// Type guard using the same source of truth
-export const isValidToolId = (value: string): value is ToolId => {
- return TOOL_IDS.includes(value as ToolId);
-};
diff --git a/frontend/src/utils/automationExecutor.ts b/frontend/src/utils/automationExecutor.ts
index cb34d3407..8c59be9b1 100644
--- a/frontend/src/utils/automationExecutor.ts
+++ b/frontend/src/utils/automationExecutor.ts
@@ -1,5 +1,6 @@
import axios from 'axios';
import { ToolRegistry } from '../data/toolsTaxonomy';
+import { ToolId } from '../types/toolId';
import { AUTOMATION_CONSTANTS } from '../constants/automation';
import { AutomationFileProcessor } from './automationFileProcessor';
import { ToolType } from '../hooks/tools/shared/useToolOperation';
@@ -149,7 +150,7 @@ export const executeToolOperationWithPrefix = async (
toolRegistry: ToolRegistry,
filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX
): Promise => {
- const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
+ const config = toolRegistry[operationName as ToolId]?.operationConfig;
if (!config) {
throw new Error(`Tool operation not supported: ${operationName}`);
}
diff --git a/frontend/src/utils/signatureFlattening.ts b/frontend/src/utils/signatureFlattening.ts
index ccb739253..062313df5 100644
--- a/frontend/src/utils/signatureFlattening.ts
+++ b/frontend/src/utils/signatureFlattening.ts
@@ -2,7 +2,7 @@ import { PDFDocument, rgb } from 'pdf-lib';
import { generateThumbnailWithMetadata } from './thumbnailUtils';
import { createProcessedFile, createChildStub } from '../contexts/file/fileActions';
import { createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '../types/fileContext';
-import type { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
+import type { SignatureAPI } from '../components/viewer/viewerTypes';
interface MinimalFileContextSelectors {
getAllFileIds: () => FileId[];
@@ -319,4 +319,4 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
console.error('Error flattening signatures:', error);
return null;
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/utils/toolSearch.ts b/frontend/src/utils/toolSearch.ts
index b2ec9b590..595bf7c19 100644
--- a/frontend/src/utils/toolSearch.ts
+++ b/frontend/src/utils/toolSearch.ts
@@ -1,5 +1,5 @@
import { ToolId } from "src/types/toolId";
-import { ToolRegistryEntry } from "../data/toolsTaxonomy";
+import { ToolRegistryEntry, ToolRegistry } from "../data/toolsTaxonomy";
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
export interface RankedToolItem {
@@ -8,7 +8,7 @@ export interface RankedToolItem {
}
export function filterToolRegistryByQuery(
- toolRegistry: Record,
+ toolRegistry: Partial,
query: string
): RankedToolItem[] {
const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
@@ -96,5 +96,3 @@ export function filterToolRegistryByQuery(
return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
}
-
-
diff --git a/frontend/src/utils/urlRouting.ts b/frontend/src/utils/urlRouting.ts
index 14bc3752f..12abf2ae0 100644
--- a/frontend/src/utils/urlRouting.ts
+++ b/frontend/src/utils/urlRouting.ts
@@ -115,4 +115,3 @@ export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): stri
const tool = registry[toolId];
return tool ? tool.name : toolId;
}
-