Consistently refer to IDs via types for categories, subcategories and tools. Also fixes related issues with subcategory name rendering

This commit is contained in:
James Brunton 2025-08-21 15:02:32 +01:00
parent ec3cac484d
commit c2feebc6a5
13 changed files with 445 additions and 355 deletions

View File

@ -1925,6 +1925,11 @@
"noToolsFound": "No tools found", "noToolsFound": "No tools found",
"allTools": "ALL TOOLS", "allTools": "ALL TOOLS",
"quickAccess": "QUICK ACCESS", "quickAccess": "QUICK ACCESS",
"categories": {
"standardTools": "Standard Tools",
"advancedTools": "Advanced Tools",
"recommendedTools": "Recommended Tools"
},
"subcategories": { "subcategories": {
"signing": "Signing", "signing": "Signing",
"documentSecurity": "Document Security", "documentSecurity": "Document Security",

View File

@ -14,6 +14,7 @@ import PageEditorControls from '../pageEditor/PageEditorControls';
import Viewer from '../viewer/Viewer'; import Viewer from '../viewer/Viewer';
import ToolRenderer from '../tools/ToolRenderer'; import ToolRenderer from '../tools/ToolRenderer';
import LandingPage from '../shared/LandingPage'; import LandingPage from '../shared/LandingPage';
import { ToolId } from '../../data/toolsTaxonomy';
// No props needed - component uses contexts directly // No props needed - component uses contexts directly
export default function Workbench() { export default function Workbench() {
@ -42,15 +43,15 @@ export default function Workbench() {
const handlePreviewClose = () => { const handlePreviewClose = () => {
setPreviewFile(null); setPreviewFile(null);
const previousMode = sessionStorage.getItem('previousMode'); const previousMode = sessionStorage.getItem('previousMode');
if (previousMode === 'split') { if (previousMode === ToolId.SPLIT_PDF) {
// Use context's handleToolSelect which coordinates tool selection and view changes // Use context's handleToolSelect which coordinates tool selection and view changes
handleToolSelect('split'); handleToolSelect(ToolId.SPLIT_PDF);
sessionStorage.removeItem('previousMode'); sessionStorage.removeItem('previousMode');
} else if (previousMode === 'compress') { } else if (previousMode === ToolId.COMPRESS) {
handleToolSelect('compress'); handleToolSelect(ToolId.COMPRESS);
sessionStorage.removeItem('previousMode'); sessionStorage.removeItem('previousMode');
} else if (previousMode === 'convert') { } else if (previousMode === ToolId.CONVERT) {
handleToolSelect('convert'); handleToolSelect(ToolId.CONVERT);
sessionStorage.removeItem('previousMode'); sessionStorage.removeItem('previousMode');
} else { } else {
setCurrentView('fileEditor'); setCurrentView('fileEditor');

View File

@ -13,11 +13,12 @@ import { ButtonConfig } from '../../types/sidebar';
import './quickAccessBar/QuickAccessBar.css'; import './quickAccessBar/QuickAccessBar.css';
import AllToolsNavButton from './AllToolsNavButton'; import AllToolsNavButton from './AllToolsNavButton';
import ActiveToolButton from "./quickAccessBar/ActiveToolButton"; import ActiveToolButton from "./quickAccessBar/ActiveToolButton";
import { import {
isNavButtonActive, isNavButtonActive,
getNavButtonStyle, getNavButtonStyle,
getActiveNavButton, getActiveNavButton,
} from './quickAccessBar/QuickAccessBar'; } from './quickAccessBar/QuickAccessBar';
import { ToolId } from "../../data/toolsTaxonomy";
const QuickAccessBar = forwardRef<HTMLDivElement>(({ const QuickAccessBar = forwardRef<HTMLDivElement>(({
}, ref) => { }, ref) => {
@ -39,23 +40,23 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
openFilesModal(); openFilesModal();
}; };
const buttonConfigs: ButtonConfig[] = [ const buttonConfigs: ButtonConfig[] = [
{ {
id: 'read', id: ToolId.READ,
name: t("quickAccess.read", "Read"), name: t("quickAccess.read", "Read"),
icon: <MenuBookIcon sx={{ fontSize: "1.5rem" }} />, icon: <MenuBookIcon sx={{ fontSize: "1.5rem" }} />,
size: 'lg', size: 'lg',
isRound: false, isRound: false,
type: 'navigation', type: 'navigation',
onClick: () => { onClick: () => {
setActiveButton('read'); setActiveButton(ToolId.READ);
handleBackToTools(); handleBackToTools();
handleReaderToggle(); handleReaderToggle();
} }
}, },
{ {
id: 'sign', id: ToolId.SIGN,
name: t("quickAccess.sign", "Sign"), name: t("quickAccess.sign", "Sign"),
icon: icon:
<span className="material-symbols-rounded font-size-20"> <span className="material-symbols-rounded font-size-20">
@ -66,11 +67,11 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
type: 'navigation', type: 'navigation',
onClick: () => { onClick: () => {
setActiveButton('sign'); setActiveButton('sign');
handleToolSelect('sign'); handleToolSelect(ToolId.SIGN);
} }
}, },
{ {
id: 'automate', id: ToolId.AUTOMATE,
name: t("quickAccess.automate", "Automate"), name: t("quickAccess.automate", "Automate"),
icon: icon:
<span className="material-symbols-rounded font-size-20"> <span className="material-symbols-rounded font-size-20">
@ -80,8 +81,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
isRound: false, isRound: false,
type: 'navigation', type: 'navigation',
onClick: () => { onClick: () => {
setActiveButton('automate'); setActiveButton(ToolId.AUTOMATE);
handleToolSelect('automate'); handleToolSelect(ToolId.AUTOMATE);
} }
}, },
{ {
@ -226,4 +227,4 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
); );
}); });
export default QuickAccessBar; export default QuickAccessBar;

View File

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Box, Stack, Text } from '@mantine/core'; import { Box, Stack, Text } from '@mantine/core';
import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; import { getSubcategoryLabel, ToolId, ToolRegistryEntry } from '../../data/toolsTaxonomy';
import ToolButton from './toolPicker/ToolButton'; import ToolButton from './toolPicker/ToolButton';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolSections } from '../../hooks/useToolSections'; import { useToolSections } from '../../hooks/useToolSections';
@ -8,8 +8,8 @@ import SubcategoryHeader from './shared/SubcategoryHeader';
import NoToolsFound from './shared/NoToolsFound'; import NoToolsFound from './shared/NoToolsFound';
interface SearchResultsProps { interface SearchResultsProps {
filteredTools: [string, ToolRegistryEntry][]; filteredTools: [ToolId, ToolRegistryEntry][];
onSelect: (id: string) => void; onSelect: (id: ToolId) => void;
} }
const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }) => { const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }) => {
@ -23,8 +23,8 @@ const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }
return ( return (
<Stack p="sm" gap="xs"> <Stack p="sm" gap="xs">
{searchGroups.map(group => ( {searchGroups.map(group => (
<Box key={group.subcategory} w="100%"> <Box key={group.subcategoryId} w="100%">
<SubcategoryHeader label={t(`toolPicker.subcategories.${group.subcategory}`, group.subcategory)} /> <SubcategoryHeader label={getSubcategoryLabel(t, group.subcategoryId)} />
<Stack gap="xs"> <Stack gap="xs">
{group.tools.map(({ id, tool }) => ( {group.tools.map(({ id, tool }) => (
<ToolButton <ToolButton

View File

@ -1,33 +1,35 @@
import React, { useMemo, useRef, useLayoutEffect, useState } from "react"; import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
import { Box, Text, Stack } from "@mantine/core"; import { Box, Text, Stack } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ToolRegistryEntry } from "../../data/toolsTaxonomy"; import { getSubcategoryLabel, ToolId, ToolRegistryEntry } from "../../data/toolsTaxonomy";
import ToolButton from "./toolPicker/ToolButton"; import ToolButton from "./toolPicker/ToolButton";
import "./toolPicker/ToolPicker.css"; import "./toolPicker/ToolPicker.css";
import { useToolSections } from "../../hooks/useToolSections"; import { SubcategoryGroup, useToolSections } from "../../hooks/useToolSections";
import SubcategoryHeader from "./shared/SubcategoryHeader"; import SubcategoryHeader from "./shared/SubcategoryHeader";
import NoToolsFound from "./shared/NoToolsFound"; import NoToolsFound from "./shared/NoToolsFound";
import { TFunction } from "i18next";
interface ToolPickerProps { interface ToolPickerProps {
selectedToolKey: string | null; selectedToolKey: ToolId | null;
onSelect: (id: string) => void; onSelect: (id: ToolId) => void;
filteredTools: [string, ToolRegistryEntry][]; filteredTools: [ToolId, ToolRegistryEntry][];
isSearching?: boolean; isSearching?: boolean;
} }
// Helper function to render tool buttons for a subcategory // Helper function to render tool buttons for a subcategory
const renderToolButtons = ( const renderToolButtons = (
subcategory: any, t: TFunction,
selectedToolKey: string | null, subcategory: SubcategoryGroup,
onSelect: (id: string) => void, selectedToolKey: ToolId | null,
onSelect: (id: ToolId) => void,
showSubcategoryHeader: boolean = true showSubcategoryHeader: boolean = true
) => ( ) => (
<Box key={subcategory.subcategory} w="100%"> <Box key={subcategory.subcategoryId} w="100%">
{showSubcategoryHeader && ( {showSubcategoryHeader && (
<SubcategoryHeader label={subcategory.subcategory} /> <SubcategoryHeader label={getSubcategoryLabel(t, subcategory.subcategoryId)} />
)} )}
<Stack gap="xs"> <Stack gap="xs">
{subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => ( {subcategory.tools.map(({ id, tool }) => (
<ToolButton <ToolButton
key={id} key={id}
id={id} id={id}
@ -69,11 +71,11 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
const { sections: visibleSections } = useToolSections(filteredTools); const { sections: visibleSections } = useToolSections(filteredTools);
const quickSection = useMemo( const quickSection = useMemo(
() => visibleSections.find(s => (s as any).key === 'quick'), () => visibleSections.find(s => s.key === 'quick'),
[visibleSections] [visibleSections]
); );
const allSection = useMemo( const allSection = useMemo(
() => visibleSections.find(s => (s as any).key === 'all'), () => visibleSections.find(s => s.key === 'all'),
[visibleSections] [visibleSections]
); );
@ -120,7 +122,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
{searchGroups.length === 0 ? ( {searchGroups.length === 0 ? (
<NoToolsFound /> <NoToolsFound />
) : ( ) : (
searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect)) searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect))
)} )}
</Stack> </Stack>
) : ( ) : (
@ -164,8 +166,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
<Box ref={quickAccessRef} w="100%"> <Box ref={quickAccessRef} w="100%">
<Stack p="sm" gap="xs"> <Stack p="sm" gap="xs">
{quickSection?.subcategories.map(sc => {quickSection?.subcategories.map(sc =>
renderToolButtons(sc, selectedToolKey, onSelect, false) renderToolButtons(t, sc, selectedToolKey, onSelect, false)
)} )}
</Stack> </Stack>
</Box> </Box>
@ -210,8 +212,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
<Box ref={allToolsRef} w="100%"> <Box ref={allToolsRef} w="100%">
<Stack p="sm" gap="xs"> <Stack p="sm" gap="xs">
{allSection?.subcategories.map(sc => {allSection?.subcategories.map(sc =>
renderToolButtons(sc, selectedToolKey, onSelect, true) renderToolButtons(t, sc, selectedToolKey, onSelect, true)
)} )}
</Stack> </Stack>
</Box> </Box>

View File

@ -21,7 +21,7 @@ export function SuggestedToolsSection(): React.ReactElement {
const IconComponent = tool.icon; const IconComponent = tool.icon;
return ( return (
<Card <Card
key={tool.name} key={tool.id}
p="sm" p="sm"
withBorder withBorder
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}

View File

@ -1,22 +1,22 @@
import React from "react"; import React from "react";
import { Button } from "@mantine/core"; import { Button } from "@mantine/core";
import { Tooltip } from "../../shared/Tooltip"; import { Tooltip } from "../../shared/Tooltip";
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; import { ToolId, ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import FitText from "../../shared/FitText"; import FitText from "../../shared/FitText";
interface ToolButtonProps { interface ToolButtonProps {
id: string; id: ToolId;
tool: ToolRegistryEntry; tool: ToolRegistryEntry;
isSelected: boolean; isSelected: boolean;
onSelect: (id: string) => void; onSelect: (id: ToolId) => void;
} }
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => { const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => {
const handleClick = (id: string) => { const handleClick = (id: ToolId) => {
if (tool.link) { if (tool.link) {
// Open external link in new tab // Open external link in new tab
window.open(tool.link, '_blank', 'noopener,noreferrer'); window.open(tool.link, '_blank', 'noopener,noreferrer');
return; return;
} }
// Normal tool selection // Normal tool selection
onSelect(id); onSelect(id);
@ -47,4 +47,4 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect
); );
}; };
export default ToolButton; export default ToolButton;

View File

@ -6,7 +6,7 @@
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 { ToolId, ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync'; import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
// State interface // State interface
@ -69,10 +69,10 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
// Context value interface // Context value interface
interface ToolWorkflowContextValue extends ToolWorkflowState { interface ToolWorkflowContextValue extends ToolWorkflowState {
// Tool management (from hook) // Tool management (from hook)
selectedToolKey: string | null; selectedToolKey: ToolId | null;
selectedTool: ToolRegistryEntry | null; selectedTool: ToolRegistryEntry | null;
toolRegistry: any; // From useToolManagement toolRegistry: any; // From useToolManagement
// UI Actions // UI Actions
setSidebarsVisible: (visible: boolean) => void; setSidebarsVisible: (visible: boolean) => void;
setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
@ -82,16 +82,16 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
setSearchQuery: (query: string) => void; setSearchQuery: (query: string) => void;
// Tool Actions // Tool Actions
selectTool: (toolId: string) => void; selectTool: (toolId: ToolId) => void;
clearToolSelection: () => void; clearToolSelection: () => void;
// Workflow Actions (compound actions) // Workflow Actions (compound actions)
handleToolSelect: (toolId: string) => void; handleToolSelect: (toolId: ToolId) => void;
handleBackToTools: () => void; handleBackToTools: () => void;
handleReaderToggle: () => void; handleReaderToggle: () => void;
// Computed values // Computed values
filteredTools: [string, ToolRegistryEntry][]; // Filtered by search filteredTools: [ToolId, ToolRegistryEntry][]; // Filtered by search
isPanelVisible: boolean; isPanelVisible: boolean;
} }
@ -144,9 +144,9 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
}, []); }, []);
// 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: ToolId) => {
// Special-case: if tool is a dedicated reader tool, enter reader mode and do not go to toolContent // Special-case: if tool is a dedicated reader tool, enter reader mode and do not go to toolContent
if (toolId === 'read' || toolId === 'view-pdf') { if (toolId === ToolId.READ) {
setReaderMode(true); setReaderMode(true);
setLeftPanelView('toolPicker'); setLeftPanelView('toolPicker');
clearToolSelection(); clearToolSelection();
@ -175,7 +175,7 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
// Filter tools based on search query // Filter tools based on search query
const filteredTools = useMemo(() => { const filteredTools = useMemo(() => {
if (!toolRegistry) return []; if (!toolRegistry) return [];
return Object.entries(toolRegistry).filter(([_, { name }]) => return (Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][]).filter(([_, { name }]) =>
name.toLowerCase().includes(state.searchQuery.toLowerCase()) name.toLowerCase().includes(state.searchQuery.toLowerCase())
); );
}, [toolRegistry, state.searchQuery]); }, [toolRegistry, state.searchQuery]);
@ -230,4 +230,4 @@ export function useToolWorkflow(): ToolWorkflowContextValue {
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider'); throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
} }
return context; return context;
} }

View File

@ -2,102 +2,160 @@ import { type TFunction } from 'i18next';
import React from 'react'; import React from 'react';
import { BaseToolProps } from '../types/tool'; import { BaseToolProps } from '../types/tool';
export enum ToolId {
CERT_SIGN = 'certSign',
SIGN = 'sign',
ADD_PASSWORD = 'addPassword',
WATERMARK = 'watermark',
ADD_STAMP = 'add-stamp',
SANITIZE = 'sanitize',
FLATTEN = 'flatten',
UNLOCK_PDF_FORMS = 'unlock-pdf-forms',
MANAGE_CERTIFICATES = 'manage-certificates',
CHANGE_PERMISSIONS = 'change-permissions',
GET_ALL_INFO_ON_PDF = 'get-all-info-on-pdf',
VALIDATE_PDF_SIGNATURE = 'validate-pdf-signature',
READ = 'read',
CHANGE_METADATA = 'change-metadata',
CROP_PDF = 'cropPdf',
ROTATE = 'rotate',
SPLIT_PDF = 'splitPdf',
REORGANIZE_PAGES = 'reorganize-pages',
ADJUST_PAGE_SIZE_SCALE = 'adjust-page-size-scale',
ADD_PAGE_NUMBERS = 'addPageNumbers',
MULTI_PAGE_LAYOUT = 'multi-page-layout',
SINGLE_LARGE_PAGE = 'single-large-page',
ADD_ATTACHMENTS = 'add-attachments',
EXTRACT_PAGES = 'extractPages',
EXTRACT_IMAGES = 'extract-images',
REMOVE_PAGES = 'removePages',
REMOVE_BLANK_PAGES = 'remove-blank-pages',
REMOVE_ANNOTATIONS = 'remove-annotations',
REMOVE_IMAGE = 'remove-image',
REMOVE_PASSWORD = 'remove-password',
REMOVE_CERTIFICATE_SIGN = 'remove-certificate-sign',
AUTOMATE = 'automate',
AUTO_RENAME_PDF_FILE = 'auto-rename-pdf-file',
AUTO_SPLIT_PAGES = 'auto-split-pages',
AUTO_SPLIT_BY_SIZE_COUNT = 'auto-split-by-size-count',
ADJUST_CONTRAST = 'adjustContrast',
REPAIR = 'repair',
DETECT_SPLIT_SCANNED_PHOTOS = 'detect-split-scanned-photos',
OVERLAY_PDFS = 'overlay-pdfs',
REPLACE_AND_INVERT_COLOR = 'replace-and-invert-color',
ADD_IMAGE = 'add-image',
EDIT_TABLE_OF_CONTENTS = 'edit-table-of-contents',
SCANNER_EFFECT = 'scanner-effect',
SHOW_JAVASCRIPT = 'show-javascript',
DEV_API = 'dev-api',
DEV_FOLDER_SCANNING = 'dev-folder-scanning',
DEV_SSO_GUIDE = 'dev-sso-guide',
DEV_AIRGAPPED = 'dev-airgapped',
COMPARE = 'compare',
COMPRESS = 'compress',
CONVERT = 'convert',
MERGE_PDFS = 'mergePdfs',
MULTI_TOOL = 'multi-tool',
OCR = 'ocr',
REDACT = 'redact'
};
export enum SubcategoryId { export enum SubcategoryId {
SIGNING = 'signing', SIGNING = 'signing',
DOCUMENT_SECURITY = 'documentSecurity', DOCUMENT_SECURITY = 'documentSecurity',
VERIFICATION = 'verification', VERIFICATION = 'verification',
DOCUMENT_REVIEW = 'documentReview', DOCUMENT_REVIEW = 'documentReview',
PAGE_FORMATTING = 'pageFormatting', PAGE_FORMATTING = 'pageFormatting',
EXTRACTION = 'extraction', EXTRACTION = 'extraction',
REMOVAL = 'removal', REMOVAL = 'removal',
AUTOMATION = 'automation', AUTOMATION = 'automation',
GENERAL = 'general', GENERAL = 'general',
ADVANCED_FORMATTING = 'advancedFormatting', ADVANCED_FORMATTING = 'advancedFormatting',
DEVELOPER_TOOLS = 'developerTools' DEVELOPER_TOOLS = 'developerTools'
} }
export enum ToolCategory { export enum ToolCategoryId {
STANDARD_TOOLS = 'Standard Tools', STANDARD_TOOLS = 'standardTools',
ADVANCED_TOOLS = 'Advanced Tools', ADVANCED_TOOLS = 'advancedTools',
RECOMMENDED_TOOLS = 'Recommended Tools' RECOMMENDED_TOOLS = 'recommendedTools'
} }
export type ToolRegistryEntry = { 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'; view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
description: string; description: string;
category: ToolCategory; categoryId: ToolCategoryId;
subcategory: SubcategoryId; subcategoryId: SubcategoryId;
maxFiles?: number; maxFiles?: number;
supportedFormats?: string[]; supportedFormats?: string[];
endpoints?: string[]; endpoints?: string[];
link?: string; link?: string;
type?: string; type?: string;
} }
export type ToolRegistry = Record<string, ToolRegistryEntry>; export type ToolRegistry = Record<ToolId, ToolRegistryEntry>;
export const SUBCATEGORY_ORDER: SubcategoryId[] = [ export const SUBCATEGORY_ORDER: SubcategoryId[] = [
SubcategoryId.SIGNING, SubcategoryId.SIGNING,
SubcategoryId.DOCUMENT_SECURITY, SubcategoryId.DOCUMENT_SECURITY,
SubcategoryId.VERIFICATION, SubcategoryId.VERIFICATION,
SubcategoryId.DOCUMENT_REVIEW, SubcategoryId.DOCUMENT_REVIEW,
SubcategoryId.PAGE_FORMATTING, SubcategoryId.PAGE_FORMATTING,
SubcategoryId.EXTRACTION, SubcategoryId.EXTRACTION,
SubcategoryId.REMOVAL, SubcategoryId.REMOVAL,
SubcategoryId.AUTOMATION, SubcategoryId.AUTOMATION,
SubcategoryId.GENERAL, SubcategoryId.GENERAL,
SubcategoryId.ADVANCED_FORMATTING, SubcategoryId.ADVANCED_FORMATTING,
SubcategoryId.DEVELOPER_TOOLS, SubcategoryId.DEVELOPER_TOOLS,
]; ];
export const SUBCATEGORY_COLOR_MAP: Record<SubcategoryId, string> = { export const SUBCATEGORY_COLOR_MAP: Record<SubcategoryId, string> = {
[SubcategoryId.SIGNING]: '#FF7892', [SubcategoryId.SIGNING]: '#FF7892',
[SubcategoryId.DOCUMENT_SECURITY]: '#FF7892', [SubcategoryId.DOCUMENT_SECURITY]: '#FF7892',
[SubcategoryId.VERIFICATION]: '#1BB1D4', [SubcategoryId.VERIFICATION]: '#1BB1D4',
[SubcategoryId.DOCUMENT_REVIEW]: '#48BD54', [SubcategoryId.DOCUMENT_REVIEW]: '#48BD54',
[SubcategoryId.PAGE_FORMATTING]: '#7882FF', [SubcategoryId.PAGE_FORMATTING]: '#7882FF',
[SubcategoryId.EXTRACTION]: '#1BB1D4', [SubcategoryId.EXTRACTION]: '#1BB1D4',
[SubcategoryId.REMOVAL]: '#7882FF', [SubcategoryId.REMOVAL]: '#7882FF',
[SubcategoryId.AUTOMATION]: '#69DC95', [SubcategoryId.AUTOMATION]: '#69DC95',
[SubcategoryId.GENERAL]: '#69DC95', [SubcategoryId.GENERAL]: '#69DC95',
[SubcategoryId.ADVANCED_FORMATTING]: '#F55454', [SubcategoryId.ADVANCED_FORMATTING]: '#F55454',
[SubcategoryId.DEVELOPER_TOOLS]: '#F55454', [SubcategoryId.DEVELOPER_TOOLS]: '#F55454',
}; };
export const getSubcategoryColor = (subcategory: SubcategoryId): string => SUBCATEGORY_COLOR_MAP[subcategory] || '#7882FF'; export const getCategoryLabel = (t: TFunction, id: ToolCategoryId): string => t(`toolPicker.categories.${id}`, id);
export const getSubcategoryLabel = (t: TFunction, id: SubcategoryId): string => t(`toolPicker.subcategories.${id}`, id); export const getSubcategoryLabel = (t: TFunction, id: SubcategoryId): string => t(`toolPicker.subcategories.${id}`, id);
export const getSubcategoryColor = (subcategory: SubcategoryId): string => SUBCATEGORY_COLOR_MAP[subcategory] || '#7882FF';
export const getAllEndpoints = (registry: ToolRegistry): string[] => { export const getAllEndpoints = (registry: ToolRegistry): string[] => {
const lists: string[][] = []; const lists: string[][] = [];
Object.values(registry).forEach(entry => { Object.values(registry).forEach(entry => {
if (entry.endpoints && entry.endpoints.length > 0) { if (entry.endpoints && entry.endpoints.length > 0) {
lists.push(entry.endpoints); lists.push(entry.endpoints);
} }
}); });
return Array.from(new Set(lists.flat())); return Array.from(new Set(lists.flat()));
}; };
export const getConversionEndpoints = (extensionToEndpoint: Record<string, Record<string, string>>): string[] => { export const getConversionEndpoints = (extensionToEndpoint: Record<string, Record<string, string>>): string[] => {
const endpoints = new Set<string>(); const endpoints = new Set<string>();
Object.values(extensionToEndpoint).forEach(toEndpoints => { Object.values(extensionToEndpoint).forEach(toEndpoints => {
Object.values(toEndpoints).forEach(endpoint => { Object.values(toEndpoints).forEach(endpoint => {
endpoints.add(endpoint); endpoints.add(endpoint);
}); });
}); });
return Array.from(endpoints); return Array.from(endpoints);
}; };
export const getAllApplicationEndpoints = ( export const getAllApplicationEndpoints = (
registry: ToolRegistry, registry: ToolRegistry,
extensionToEndpoint?: Record<string, Record<string, string>> extensionToEndpoint?: Record<string, Record<string, string>>
): string[] => { ): string[] => {
const toolEp = getAllEndpoints(registry); const toolEp = getAllEndpoints(registry);
const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : [];
return Array.from(new Set([...toolEp, ...convEp])); return Array.from(new Set([...toolEp, ...convEp]));
}; };

View File

@ -1,4 +1,3 @@
import React from 'react';
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";
@ -8,7 +7,7 @@ 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, ToolCategory, ToolRegistry } from './toolsTaxonomy'; import { SubcategoryId, ToolCategoryId, ToolId, 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';
@ -24,319 +23,319 @@ export function useFlatToolRegistry(): ToolRegistry {
const allTools: ToolRegistry = { const allTools: ToolRegistry = {
// Signing // Signing
"certSign": { [ToolId.CERT_SIGN]: {
icon: <span className="material-symbols-rounded">workspace_premium</span>, icon: <span className="material-symbols-rounded">workspace_premium</span>,
name: t("home.certSign.title", "Sign with Certificate"), name: t("home.certSign.title", "Sign with Certificate"),
component: null, component: null,
view: "sign", 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)"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.SIGNING subcategoryId: SubcategoryId.SIGNING
}, },
"sign": { [ToolId.SIGN]: {
icon: <span className="material-symbols-rounded">signature</span>, icon: <span className="material-symbols-rounded">signature</span>,
name: t("home.sign.title", "Sign"), name: t("home.sign.title", "Sign"),
component: null, component: null,
view: "sign", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.SIGNING subcategoryId: SubcategoryId.SIGNING
}, },
// Document Security // Document Security
"addPassword": { [ToolId.ADD_PASSWORD]: {
icon: <span className="material-symbols-rounded">password</span>, icon: <span className="material-symbols-rounded">password</span>,
name: t("home.addPassword.title", "Add Password"), name: t("home.addPassword.title", "Add Password"),
component: AddPassword, component: AddPassword,
view: "security", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1, maxFiles: -1,
endpoints: ["add-password"] endpoints: ["add-password"]
}, },
"watermark": { [ToolId.WATERMARK]: {
icon: <span className="material-symbols-rounded">branding_watermark</span>, icon: <span className="material-symbols-rounded">branding_watermark</span>,
name: t("home.watermark.title", "Add Watermark"), name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark, component: AddWatermark,
view: "format", 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."),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
endpoints: ["add-watermark"] endpoints: ["add-watermark"]
}, },
"add-stamp": { [ToolId.ADD_STAMP]: {
icon: <span className="material-symbols-rounded">approval</span>, icon: <span className="material-symbols-rounded">approval</span>,
name: t("home.AddStampRequest.title", "Add Stamp to PDF"), name: t("home.AddStampRequest.title", "Add Stamp to PDF"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY subcategoryId: SubcategoryId.DOCUMENT_SECURITY
}, },
"sanitize": { [ToolId.SANITIZE]: {
icon: <span className="material-symbols-rounded">cleaning_services</span>, icon: <span className="material-symbols-rounded">cleaning_services</span>,
name: t("home.sanitize.title", "Sanitize"), name: t("home.sanitize.title", "Sanitize"),
component: Sanitize, component: Sanitize,
view: "security", view: "security",
maxFiles: -1, maxFiles: -1,
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: 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"]
}, },
"flatten": { [ToolId.FLATTEN]: {
icon: <span className="material-symbols-rounded">layers_clear</span>, icon: <span className="material-symbols-rounded">layers_clear</span>,
name: t("home.flatten.title", "Flatten"), name: t("home.flatten.title", "Flatten"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY subcategoryId: SubcategoryId.DOCUMENT_SECURITY
}, },
"unlock-pdf-forms": { [ToolId.UNLOCK_PDF_FORMS]: {
icon: <span className="material-symbols-rounded">preview_off</span>, icon: <span className="material-symbols-rounded">preview_off</span>,
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"), name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
component: UnlockPdfForms, component: UnlockPdfForms,
view: "security", 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."),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1, maxFiles: -1,
endpoints: ["unlock-pdf-forms"] endpoints: ["unlock-pdf-forms"]
}, },
"manage-certificates": { [ToolId.MANAGE_CERTIFICATES]: {
icon: <span className="material-symbols-rounded">license</span>, icon: <span className="material-symbols-rounded">license</span>,
name: t("home.manageCertificates.title", "Manage Certificates"), name: t("home.manageCertificates.title", "Manage Certificates"),
component: null, component: null,
view: "security", view: "security",
description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."), description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY subcategoryId: SubcategoryId.DOCUMENT_SECURITY
}, },
"change-permissions": { [ToolId.CHANGE_PERMISSIONS]: {
icon: <span className="material-symbols-rounded">lock</span>, icon: <span className="material-symbols-rounded">lock</span>,
name: t("home.changePermissions.title", "Change Permissions"), name: t("home.changePermissions.title", "Change Permissions"),
component: ChangePermissions, component: ChangePermissions,
view: "security", view: "security",
description: t("home.changePermissions.desc", "Change document restrictions and permissions"), description: t("home.changePermissions.desc", "Change document restrictions and permissions"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1, maxFiles: -1,
endpoints: ["add-password"] endpoints: ["add-password"]
}, },
// Verification // Verification
"get-all-info-on-pdf": { [ToolId.GET_ALL_INFO_ON_PDF]: {
icon: <span className="material-symbols-rounded">fact_check</span>, icon: <span className="material-symbols-rounded">fact_check</span>,
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", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.VERIFICATION subcategoryId: SubcategoryId.VERIFICATION
}, },
"validate-pdf-signature": { [ToolId.VALIDATE_PDF_SIGNATURE]: {
icon: <span className="material-symbols-rounded">verified</span>, icon: <span className="material-symbols-rounded">verified</span>,
name: t("home.validateSignature.title", "Validate PDF Signature"), name: t("home.validateSignature.title", "Validate PDF Signature"),
component: null, component: null,
view: "security", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.VERIFICATION subcategoryId: SubcategoryId.VERIFICATION
}, },
// Document Review // Document Review
"read": { [ToolId.READ]: {
icon: <span className="material-symbols-rounded">article</span>, icon: <span className="material-symbols-rounded">article</span>,
name: t("home.read.title", "Read"), name: t("home.read.title", "Read"),
component: null, component: null,
view: "view", view: "view",
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."),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_REVIEW subcategoryId: SubcategoryId.DOCUMENT_REVIEW
}, },
"change-metadata": { [ToolId.CHANGE_METADATA]: {
icon: <span className="material-symbols-rounded">assignment</span>, icon: <span className="material-symbols-rounded">assignment</span>,
name: t("home.changeMetadata.title", "Change Metadata"), name: t("home.changeMetadata.title", "Change Metadata"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_REVIEW subcategoryId: SubcategoryId.DOCUMENT_REVIEW
}, },
// Page Formatting // Page Formatting
"cropPdf": { [ToolId.CROP_PDF]: {
icon: <span className="material-symbols-rounded">crop</span>, icon: <span className="material-symbols-rounded">crop</span>,
name: t("home.crop.title", "Crop PDF"), name: t("home.crop.title", "Crop PDF"),
component: null, component: null,
view: "format", 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!)"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING
}, },
"rotate": { [ToolId.ROTATE]: {
icon: <span className="material-symbols-rounded">rotate_right</span>, icon: <span className="material-symbols-rounded">rotate_right</span>,
name: t("home.rotate.title", "Rotate"), name: t("home.rotate.title", "Rotate"),
component: null, component: null,
view: "format", view: "format",
description: t("home.rotate.desc", "Easily rotate your PDFs."), description: t("home.rotate.desc", "Easily rotate your PDFs."),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING
}, },
"splitPdf": { [ToolId.SPLIT_PDF]: {
icon: <span className="material-symbols-rounded">content_cut</span>, icon: <span className="material-symbols-rounded">content_cut</span>,
name: t("home.split.title", "Split"), name: t("home.split.title", "Split"),
component: SplitPdfPanel, component: SplitPdfPanel,
view: "split", view: "split",
description: t("home.split.desc", "Split PDFs into multiple documents"), description: t("home.split.desc", "Split PDFs into multiple documents"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING
}, },
"reorganize-pages": { [ToolId.REORGANIZE_PAGES]: {
icon: <span className="material-symbols-rounded">move_down</span>, icon: <span className="material-symbols-rounded">move_down</span>,
name: t("home.reorganizePages.title", "Reorganize Pages"), name: t("home.reorganizePages.title", "Reorganize Pages"),
component: null, component: null,
view: "pageEditor", view: "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."),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING
}, },
"adjust-page-size-scale": { [ToolId.ADJUST_PAGE_SIZE_SCALE]: {
icon: <span className="material-symbols-rounded">crop_free</span>, icon: <span className="material-symbols-rounded">crop_free</span>,
name: t("home.scalePages.title", "Adjust page size/scale"), name: t("home.scalePages.title", "Adjust page size/scale"),
component: null, component: null,
view: "format", 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."),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING
}, },
"addPageNumbers": { [ToolId.ADD_PAGE_NUMBERS]: {
icon: <span className="material-symbols-rounded">123</span>, icon: <span className="material-symbols-rounded">123</span>,
name: t("home.addPageNumbers.title", "Add Page Numbers"), name: t("home.addPageNumbers.title", "Add Page Numbers"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING
}, },
"multi-page-layout": { [ToolId.MULTI_PAGE_LAYOUT]: {
icon: <span className="material-symbols-rounded">dashboard</span>, icon: <span className="material-symbols-rounded">dashboard</span>,
name: t("home.pageLayout.title", "Multi-Page Layout"), name: t("home.pageLayout.title", "Multi-Page Layout"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategoryId: SubcategoryId.PAGE_FORMATTING
}, },
"single-large-page": { [ToolId.SINGLE_LARGE_PAGE]: {
icon: <span className="material-symbols-rounded">looks_one</span>, icon: <span className="material-symbols-rounded">looks_one</span>,
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", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1, maxFiles: -1,
endpoints: ["pdf-to-single-page"] endpoints: ["pdf-to-single-page"]
}, },
"add-attachments": { [ToolId.ADD_ATTACHMENTS]: {
icon: <span className="material-symbols-rounded">attachment</span>, icon: <span className="material-symbols-rounded">attachment</span>,
name: t("home.attachments.title", "Add Attachments"), name: t("home.attachments.title", "Add Attachments"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
}, },
// Extraction // Extraction
"extractPages": { [ToolId.EXTRACT_PAGES]: {
icon: <span className="material-symbols-rounded">upload</span>, icon: <span className="material-symbols-rounded">upload</span>,
name: t("home.extractPages.title", "Extract Pages"), name: t("home.extractPages.title", "Extract Pages"),
component: null, component: null,
view: "extract", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.EXTRACTION subcategoryId: SubcategoryId.EXTRACTION
}, },
"extract-images": { [ToolId.EXTRACT_IMAGES]: {
icon: <span className="material-symbols-rounded">filter</span>, icon: <span className="material-symbols-rounded">filter</span>,
name: t("home.extractImages.title", "Extract Images"), name: t("home.extractImages.title", "Extract Images"),
component: null, component: null,
view: "extract", view: "extract",
description: t("home.extractImages.desc", "Extract images from PDF documents"), description: t("home.extractImages.desc", "Extract images from PDF documents"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.EXTRACTION subcategoryId: SubcategoryId.EXTRACTION
}, },
// Removal // Removal
"removePages": { [ToolId.REMOVE_PAGES]: {
icon: <span className="material-symbols-rounded">delete</span>, icon: <span className="material-symbols-rounded">delete</span>,
name: t("home.removePages.title", "Remove Pages"), name: t("home.removePages.title", "Remove Pages"),
component: null, component: null,
view: "remove", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL
}, },
"remove-blank-pages": { [ToolId.REMOVE_BLANK_PAGES]: {
icon: <span className="material-symbols-rounded">scan_delete</span>, icon: <span className="material-symbols-rounded">scan_delete</span>,
name: t("home.removeBlanks.title", "Remove Blank Pages"), name: t("home.removeBlanks.title", "Remove Blank Pages"),
component: null, component: null,
view: "remove", view: "remove",
description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"), description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL
}, },
"remove-annotations": { [ToolId.REMOVE_ANNOTATIONS]: {
icon: <span className="material-symbols-rounded">thread_unread</span>, icon: <span className="material-symbols-rounded">thread_unread</span>,
name: t("home.removeAnnotations.title", "Remove Annotations"), name: t("home.removeAnnotations.title", "Remove Annotations"),
component: null, component: null,
view: "remove", 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"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL
}, },
"remove-image": { [ToolId.REMOVE_IMAGE]: {
icon: <span className="material-symbols-rounded">remove_selection</span>, icon: <span className="material-symbols-rounded">remove_selection</span>,
name: t("home.removeImagePdf.title", "Remove Image"), name: t("home.removeImagePdf.title", "Remove Image"),
component: null, component: null,
view: "format", view: "format",
description: t("home.removeImagePdf.desc", "Remove images from PDF documents"), description: t("home.removeImagePdf.desc", "Remove images from PDF documents"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.REMOVAL subcategoryId: SubcategoryId.REMOVAL
}, },
"remove-password": { [ToolId.REMOVE_PASSWORD]: {
icon: <span className="material-symbols-rounded">lock_open_right</span>, icon: <span className="material-symbols-rounded">lock_open_right</span>,
name: t("home.removePassword.title", "Remove Password"), name: t("home.removePassword.title", "Remove Password"),
component: RemovePassword, component: RemovePassword,
view: "security", view: "security",
description: t("home.removePassword.desc", "Remove password protection from PDF documents"), description: t("home.removePassword.desc", "Remove password protection from PDF documents"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
endpoints: ["remove-password"], endpoints: ["remove-password"],
maxFiles: -1, maxFiles: -1,
}, },
"remove-certificate-sign": { [ToolId.REMOVE_CERTIFICATE_SIGN]: {
icon: <span className="material-symbols-rounded">remove_moderator</span>, icon: <span className="material-symbols-rounded">remove_moderator</span>,
name: t("home.removeCertSign.title", "Remove Certificate Sign"), name: t("home.removeCertSign.title", "Remove Certificate Sign"),
component: RemoveCertificateSign, component: RemoveCertificateSign,
view: "security", view: "security",
description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"), description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"),
category: ToolCategory.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategory: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
maxFiles: -1, maxFiles: -1,
endpoints: ["remove-certificate-sign"] endpoints: ["remove-certificate-sign"]
}, },
@ -344,203 +343,203 @@ export function useFlatToolRegistry(): ToolRegistry {
// Automation // Automation
"automate": { [ToolId.AUTOMATE]: {
icon: <span className="material-symbols-rounded">automation</span>, icon: <span className="material-symbols-rounded">automation</span>,
name: t("home.automate.title", "Automate"), name: t("home.automate.title", "Automate"),
component: null, component: null,
view: "format", view: "format",
description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."), description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.AUTOMATION subcategoryId: SubcategoryId.AUTOMATION
}, },
"auto-rename-pdf-file": { [ToolId.AUTO_RENAME_PDF_FILE]: {
icon: <span className="material-symbols-rounded">match_word</span>, icon: <span className="material-symbols-rounded">match_word</span>,
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", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.AUTOMATION subcategoryId: SubcategoryId.AUTOMATION
}, },
"auto-split-pages": { [ToolId.AUTO_SPLIT_PAGES]: {
icon: <span className="material-symbols-rounded">split_scene_right</span>, icon: <span className="material-symbols-rounded">split_scene_right</span>,
name: t("home.autoSplitPDF.title", "Auto Split Pages"), name: t("home.autoSplitPDF.title", "Auto Split Pages"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.AUTOMATION subcategoryId: SubcategoryId.AUTOMATION
}, },
"auto-split-by-size-count": { [ToolId.AUTO_SPLIT_BY_SIZE_COUNT]: {
icon: <span className="material-symbols-rounded">content_cut</span>, icon: <span className="material-symbols-rounded">content_cut</span>,
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", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.AUTOMATION subcategoryId: SubcategoryId.AUTOMATION
}, },
// Advanced Formatting // Advanced Formatting
"adjustContrast": { [ToolId.ADJUST_CONTRAST]: {
icon: <span className="material-symbols-rounded">palette</span>, icon: <span className="material-symbols-rounded">palette</span>,
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"), name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING
}, },
"repair": { [ToolId.REPAIR]: {
icon: <span className="material-symbols-rounded">build</span>, icon: <span className="material-symbols-rounded">build</span>,
name: t("home.repair.title", "Repair"), name: t("home.repair.title", "Repair"),
component: Repair, component: Repair,
view: "format", view: "format",
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"), description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
maxFiles: -1, maxFiles: -1,
endpoints: ["repair"] endpoints: ["repair"]
}, },
"detect-split-scanned-photos": { [ToolId.DETECT_SPLIT_SCANNED_PHOTOS]: {
icon: <span className="material-symbols-rounded">scanner</span>, icon: <span className="material-symbols-rounded">scanner</span>,
name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"), name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING
}, },
"overlay-pdfs": { [ToolId.OVERLAY_PDFS]: {
icon: <span className="material-symbols-rounded">layers</span>, icon: <span className="material-symbols-rounded">layers</span>,
name: t("home.overlay-pdfs.title", "Overlay PDFs"), name: t("home.overlay-pdfs.title", "Overlay PDFs"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING
}, },
"replace-and-invert-color": { [ToolId.REPLACE_AND_INVERT_COLOR]: {
icon: <span className="material-symbols-rounded">format_color_fill</span>, icon: <span className="material-symbols-rounded">format_color_fill</span>,
name: t("home.replaceColorPdf.title", "Replace & Invert Color"), name: t("home.replaceColorPdf.title", "Replace & Invert Color"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING
}, },
"add-image": { [ToolId.ADD_IMAGE]: {
icon: <span className="material-symbols-rounded">image</span>, icon: <span className="material-symbols-rounded">image</span>,
name: t("home.addImage.title", "Add Image"), name: t("home.addImage.title", "Add Image"),
component: null, component: null,
view: "format", view: "format",
description: t("home.addImage.desc", "Add images to PDF documents"), description: t("home.addImage.desc", "Add images to PDF documents"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING
}, },
"edit-table-of-contents": { [ToolId.EDIT_TABLE_OF_CONTENTS]: {
icon: <span className="material-symbols-rounded">bookmark_add</span>, icon: <span className="material-symbols-rounded">bookmark_add</span>,
name: t("home.editTableOfContents.title", "Edit Table of Contents"), name: t("home.editTableOfContents.title", "Edit Table of Contents"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING
}, },
"scanner-effect": { [ToolId.SCANNER_EFFECT]: {
icon: <span className="material-symbols-rounded">scanner</span>, icon: <span className="material-symbols-rounded">scanner</span>,
name: t("home.fakeScan.title", "Scanner Effect"), name: t("home.fakeScan.title", "Scanner Effect"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING subcategoryId: SubcategoryId.ADVANCED_FORMATTING
}, },
// Developer Tools // Developer Tools
"show-javascript": { [ToolId.SHOW_JAVASCRIPT]: {
icon: <span className="material-symbols-rounded">javascript</span>, icon: <span className="material-symbols-rounded">javascript</span>,
name: t("home.showJS.title", "Show JavaScript"), name: t("home.showJS.title", "Show JavaScript"),
component: null, component: null,
view: "extract", 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"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: SubcategoryId.DEVELOPER_TOOLS subcategoryId: SubcategoryId.DEVELOPER_TOOLS
}, },
"dev-api": { [ToolId.DEV_API]: {
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>, icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
name: t("home.devApi.title", "API"), name: t("home.devApi.title", "API"),
component: null, component: null,
view: "external", view: "external",
description: t("home.devApi.desc", "Link to API documentation"), description: t("home.devApi.desc", "Link to API documentation"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: 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": { [ToolId.DEV_FOLDER_SCANNING]: {
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>, icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
name: t("home.devFolderScanning.title", "Automated Folder Scanning"), name: t("home.devFolderScanning.title", "Automated Folder Scanning"),
component: null, component: null,
view: "external", view: "external",
description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"), description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: 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": { [ToolId.DEV_SSO_GUIDE]: {
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>, icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
name: t("home.devSsoGuide.title", "SSO Guide"), name: t("home.devSsoGuide.title", "SSO Guide"),
component: null, component: null,
view: "external", view: "external",
description: t("home.devSsoGuide.desc", "Link to SSO guide"), description: t("home.devSsoGuide.desc", "Link to SSO guide"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: 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": { [ToolId.DEV_AIRGAPPED]: {
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>, icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
name: t("home.devAirgapped.title", "Air-gapped Setup"), name: t("home.devAirgapped.title", "Air-gapped Setup"),
component: null, component: null,
view: "external", view: "external",
description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"), description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"),
category: ToolCategory.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategory: 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": { [ToolId.COMPARE]: {
icon: <span className="material-symbols-rounded">compare</span>, icon: <span className="material-symbols-rounded">compare</span>,
name: t("home.compare.title", "Compare"), name: t("home.compare.title", "Compare"),
component: null, component: null,
view: "format", 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"),
category: ToolCategory.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL subcategoryId: SubcategoryId.GENERAL
}, },
"compress": { [ToolId.COMPRESS]: {
icon: <span className="material-symbols-rounded">zoom_in_map</span>, icon: <span className="material-symbols-rounded">zoom_in_map</span>,
name: t("home.compress.title", "Compress"), name: t("home.compress.title", "Compress"),
component: CompressPdfPanel, component: CompressPdfPanel,
view: "compress", 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."),
category: ToolCategory.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1 maxFiles: -1
}, },
"convert": { [ToolId.CONVERT]: {
icon: <span className="material-symbols-rounded">sync_alt</span>, icon: <span className="material-symbols-rounded">sync_alt</span>,
name: t("home.convert.title", "Convert"), name: t("home.convert.title", "Convert"),
component: ConvertPanel, component: ConvertPanel,
view: "convert", 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"),
category: ToolCategory.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1, maxFiles: -1,
endpoints: [ endpoints: [
"pdf-to-img", "pdf-to-img",
@ -577,51 +576,51 @@ export function useFlatToolRegistry(): ToolRegistry {
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf" "dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
] ]
}, },
"mergePdfs": { [ToolId.MERGE_PDFS]: {
icon: <span className="material-symbols-rounded">library_add</span>, icon: <span className="material-symbols-rounded">library_add</span>,
name: t("home.merge.title", "Merge"), name: t("home.merge.title", "Merge"),
component: null, component: null,
view: "merge", 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"),
category: ToolCategory.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1 maxFiles: -1
}, },
"multi-tool": { [ToolId.MULTI_TOOL]: {
icon: <span className="material-symbols-rounded">dashboard_customize</span>, icon: <span className="material-symbols-rounded">dashboard_customize</span>,
name: t("home.multiTool.title", "Multi-Tool"), name: t("home.multiTool.title", "Multi-Tool"),
component: null, component: null,
view: "pageEditor", view: "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"),
category: ToolCategory.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1 maxFiles: -1
}, },
"ocr": { [ToolId.OCR]: {
icon: <span className="material-symbols-rounded">quick_reference_all</span>, icon: <span className="material-symbols-rounded">quick_reference_all</span>,
name: t("home.ocr.title", "OCR"), name: t("home.ocr.title", "OCR"),
component: OCRPanel, component: OCRPanel,
view: "convert", 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"),
category: ToolCategory.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1 maxFiles: -1
}, },
"redact": { [ToolId.REDACT]: {
icon: <span className="material-symbols-rounded">visibility_off</span>, icon: <span className="material-symbols-rounded">visibility_off</span>,
name: t("home.redact.title", "Redact"), name: t("home.redact.title", "Redact"),
component: null, component: null,
view: "redact", 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"),
category: ToolCategory.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL subcategoryId: SubcategoryId.GENERAL
}, },
}; };
if (showPlaceholderTools) { if (showPlaceholderTools) {
return allTools; return allTools;
} else { } else {
const filteredTools = Object.keys(allTools) const filteredTools = (Object.keys(allTools) as ToolId[])
.filter(key => allTools[key].component !== null || allTools[key].link) .filter(key => allTools[key].component !== null || allTools[key].link)
.reduce((obj, key) => { .reduce((obj, key) => {
obj[key] = allTools[key]; obj[key] = allTools[key];

View File

@ -7,9 +7,10 @@ import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import CleaningServicesIcon from '@mui/icons-material/CleaningServices'; import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
import CropIcon from '@mui/icons-material/Crop'; import CropIcon from '@mui/icons-material/Crop';
import TextFieldsIcon from '@mui/icons-material/TextFields'; import TextFieldsIcon from '@mui/icons-material/TextFields';
import { ToolId } from '../data/toolsTaxonomy';
export interface SuggestedTool { export interface SuggestedTool {
name: string; id: ToolId;
title: string; title: string;
icon: React.ComponentType<any>; icon: React.ComponentType<any>;
navigate: () => void; navigate: () => void;
@ -17,27 +18,27 @@ export interface SuggestedTool {
const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
{ {
name: 'compress', id: ToolId.COMPRESS,
title: 'Compress', title: 'Compress',
icon: CompressIcon icon: CompressIcon
}, },
{ {
name: 'convert', id: ToolId.CONVERT,
title: 'Convert', title: 'Convert',
icon: SwapHorizIcon icon: SwapHorizIcon
}, },
{ {
name: 'sanitize', id: ToolId.SANITIZE,
title: 'Sanitize', title: 'Sanitize',
icon: CleaningServicesIcon icon: CleaningServicesIcon
}, },
{ {
name: 'split', id: ToolId.SPLIT_PDF,
title: 'Split', title: 'Split',
icon: CropIcon icon: CropIcon
}, },
{ {
name: 'ocr', id: ToolId.OCR,
title: 'OCR', title: 'OCR',
icon: TextFieldsIcon icon: TextFieldsIcon
} }
@ -48,12 +49,12 @@ export function useSuggestedTools(): SuggestedTool[] {
return useMemo(() => { return useMemo(() => {
// Filter out the current tool // Filter out the current tool
const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.name !== selectedToolKey); const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedToolKey);
// Add navigation function to each tool // Add navigation function to each tool
return filteredTools.map(tool => ({ return filteredTools.map(tool => ({
...tool, ...tool,
navigate: () => handleToolSelect(tool.name) navigate: () => handleToolSelect(tool.id)
})); }));
}, [selectedToolKey, handleToolSelect]); }, [selectedToolKey, handleToolSelect]);
} }

View File

@ -1,15 +1,15 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react'; import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy"; import { getAllEndpoints, ToolId, type ToolRegistryEntry } from "../data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
interface ToolManagementResult { interface ToolManagementResult {
selectedToolKey: string | null; selectedToolKey: ToolId | null;
selectedTool: ToolRegistryEntry | null; selectedTool: ToolRegistryEntry | null;
toolSelectedFileIds: string[]; toolSelectedFileIds: string[];
toolRegistry: Record<string, ToolRegistryEntry>; toolRegistry: Record<string, ToolRegistryEntry>;
selectTool: (toolKey: string) => void; selectTool: (toolKey: ToolId) => void;
clearToolSelection: () => void; clearToolSelection: () => void;
setToolSelectedFileIds: (fileIds: string[]) => void; setToolSelectedFileIds: (fileIds: string[]) => void;
} }
@ -17,7 +17,7 @@ interface ToolManagementResult {
export const useToolManagement = (): ToolManagementResult => { export const useToolManagement = (): ToolManagementResult => {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(null); const [selectedToolKey, setSelectedToolKey] = useState<ToolId | null>(null);
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]); const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
// Build endpoints list from registry entries with fallback to legacy mapping // Build endpoints list from registry entries with fallback to legacy mapping
@ -35,7 +35,7 @@ export const useToolManagement = (): ToolManagementResult => {
const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]); const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints); const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
const isToolAvailable = useCallback((toolKey: string): boolean => { const isToolAvailable = useCallback((toolKey: ToolId): boolean => {
if (endpointsLoading) return true; if (endpointsLoading) return true;
const endpoints = baseRegistry[toolKey]?.endpoints || []; const endpoints = baseRegistry[toolKey]?.endpoints || [];
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true); return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
@ -43,7 +43,7 @@ export const useToolManagement = (): ToolManagementResult => {
const toolRegistry: Record<string, ToolRegistryEntry> = useMemo(() => { const toolRegistry: Record<string, ToolRegistryEntry> = useMemo(() => {
const availableToolRegistry: Record<string, ToolRegistryEntry> = {}; const availableToolRegistry: Record<string, ToolRegistryEntry> = {};
Object.keys(baseRegistry).forEach(toolKey => { (Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
if (isToolAvailable(toolKey)) { if (isToolAvailable(toolKey)) {
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry]; const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
availableToolRegistry[toolKey] = { availableToolRegistry[toolKey] = {
@ -58,7 +58,7 @@ export const useToolManagement = (): ToolManagementResult => {
useEffect(() => { useEffect(() => {
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) { if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) {
const firstAvailableTool = Object.keys(toolRegistry)[0]; const firstAvailableTool = (Object.keys(toolRegistry) as ToolId[])[0];
if (firstAvailableTool) { if (firstAvailableTool) {
setSelectedToolKey(firstAvailableTool); setSelectedToolKey(firstAvailableTool);
} else { } else {
@ -67,7 +67,7 @@ export const useToolManagement = (): ToolManagementResult => {
} }
}, [endpointsLoading, selectedToolKey, toolRegistry]); }, [endpointsLoading, selectedToolKey, toolRegistry]);
const selectTool = useCallback((toolKey: string) => { const selectTool = useCallback((toolKey: ToolId) => {
setSelectedToolKey(toolKey); setSelectedToolKey(toolKey);
}, []); }, []);

View File

@ -1,65 +1,87 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SUBCATEGORY_ORDER, ToolCategory, ToolRegistryEntry } from '../data/toolsTaxonomy'; import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolId, ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type SubcategoryIdMap = {
[subcategoryId in SubcategoryId]: Array<{ id: ToolId; tool: ToolRegistryEntry }>;
}
type GroupedTools = { type GroupedTools = {
[category: string]: { [categoryId in ToolCategoryId]: SubcategoryIdMap;
[subcategory: string]: Array<{ id: string; tool: ToolRegistryEntry }>;
};
}; };
export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) { export interface SubcategoryGroup {
subcategoryId: SubcategoryId;
tools: {
id: ToolId;
tool: ToolRegistryEntry;
}[];
};
export type ToolSectionKey = 'quick' | 'all';
export interface ToolSection {
key: ToolSectionKey;
title: string;
subcategories: SubcategoryGroup[];
};
export function useToolSections(filteredTools: [ToolId, ToolRegistryEntry][]) {
const { t } = useTranslation(); const { t } = useTranslation();
const groupedTools = useMemo(() => { const groupedTools = useMemo(() => {
const grouped: GroupedTools = {}; const grouped = {} as GroupedTools;
filteredTools.forEach(([id, tool]) => { filteredTools.forEach(([id, tool]) => {
const category = tool.category; const categoryId = tool.categoryId;
const subcategory = tool.subcategory; const subcategoryId = tool.subcategoryId;
if (!grouped[category]) grouped[category] = {}; if (!grouped[categoryId]) grouped[categoryId] = {} as SubcategoryIdMap;
if (!grouped[category][subcategory]) grouped[category][subcategory] = []; if (!grouped[categoryId][subcategoryId]) grouped[categoryId][subcategoryId] = [];
grouped[category][subcategory].push({ id, tool }); grouped[categoryId][subcategoryId].push({ id, tool });
}); });
return grouped; return grouped;
}, [filteredTools]); }, [filteredTools]);
const sections = useMemo(() => { const sections: ToolSection[] = useMemo(() => {
const getOrderIndex = (name: string) => { const getOrderIndex = (id: SubcategoryId) => {
const idx = SUBCATEGORY_ORDER.indexOf(name as any); const idx = SUBCATEGORY_ORDER.indexOf(id);
return idx === -1 ? Number.MAX_SAFE_INTEGER : idx; return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
}; };
const quick: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {}; const quick = {} as SubcategoryIdMap;
const all: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {}; const all = {} as SubcategoryIdMap;
Object.entries(groupedTools).forEach(([origCat, subs]) => { Object.entries(groupedTools).forEach(([c, subs]) => {
const upperCat = origCat.toUpperCase(); const categoryId = c as ToolCategoryId;
Object.entries(subs).forEach(([sub, tools]) => { Object.entries(subs).forEach(([s, tools]) => {
if (!all[sub]) all[sub] = []; const subcategoryId = s as SubcategoryId;
all[sub].push(...tools); if (!all[subcategoryId]) all[subcategoryId] = [];
all[subcategoryId].push(...tools);
}); });
if (upperCat === ToolCategory.RECOMMENDED_TOOLS.toUpperCase()) { if (categoryId === ToolCategoryId.RECOMMENDED_TOOLS) {
Object.entries(subs).forEach(([sub, tools]) => { Object.entries(subs).forEach(([s, tools]) => {
if (!quick[sub]) quick[sub] = []; const subcategoryId = s as SubcategoryId;
quick[sub].push(...tools); if (!quick[subcategoryId]) quick[subcategoryId] = [];
quick[subcategoryId].push(...tools);
}); });
} }
}); });
const sortSubs = (obj: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>>) => const sortSubs = (obj: SubcategoryIdMap) =>
Object.entries(obj) Object.entries(obj)
.sort(([a], [b]) => { .sort(([a], [b]) => {
const ai = getOrderIndex(a); const aId = a as SubcategoryId;
const bi = getOrderIndex(b); const bId = b as SubcategoryId;
const ai = getOrderIndex(aId);
const bi = getOrderIndex(bId);
if (ai !== bi) return ai - bi; if (ai !== bi) return ai - bi;
return a.localeCompare(b); return aId.localeCompare(bId);
}) })
.map(([subcategory, tools]) => ({ subcategory, tools })); .map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
const built = [ const built: ToolSection[] = [
{ key: 'quick', title: t('toolPicker.quickAccess', 'QUICK ACCESS'), subcategories: sortSubs(quick) }, { key: 'quick', title: t('toolPicker.quickAccess', 'QUICK ACCESS'), subcategories: sortSubs(quick) },
{ key: 'all', title: t('toolPicker.allTools', 'ALL TOOLS'), subcategories: sortSubs(all) } { key: 'all', title: t('toolPicker.allTools', 'ALL TOOLS'), subcategories: sortSubs(all) }
]; ];
@ -67,19 +89,20 @@ export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) {
return built.filter(section => section.subcategories.some(sc => sc.tools.length > 0)); return built.filter(section => section.subcategories.some(sc => sc.tools.length > 0));
}, [groupedTools]); }, [groupedTools]);
const searchGroups = useMemo(() => { const searchGroups: SubcategoryGroup[] = useMemo(() => {
const subMap: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {}; const subMap = {} as SubcategoryIdMap;
const seen = new Set<string>(); const seen = new Set<ToolId>();
filteredTools.forEach(([id, tool]) => { filteredTools.forEach(([id, tool]) => {
if (seen.has(id)) return; const toolId = id as ToolId;
seen.add(id); if (seen.has(toolId)) return;
const sub = tool.subcategory; seen.add(toolId);
const sub = tool.subcategoryId;
if (!subMap[sub]) subMap[sub] = []; if (!subMap[sub]) subMap[sub] = [];
subMap[sub].push({ id, tool }); subMap[sub].push({ id: toolId, tool });
}); });
return Object.entries(subMap) return Object.entries(subMap)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([subcategory, tools]) => ({ subcategory, tools })); .map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
}, [filteredTools]); }, [filteredTools]);
return { sections, searchGroups }; return { sections, searchGroups };