{/* Search Bar - Always visible at the top */}
-
-
+ setSearchQuery(e.currentTarget.value)}
- autoComplete="off"
- size="sm"
+ onChange={setSearchQuery}
+ toolRegistry={toolRegistry}
+ mode="filter"
/>
- {leftPanelView === 'toolPicker' ? (
+ {searchQuery.trim().length > 0 ? (
+ // Searching view (replaces both picker and content)
+
+ ) : leftPanelView === 'toolPicker' ? (
// Tool Picker View
0)}
/>
) : (
@@ -76,10 +94,12 @@ export default function ToolPanel() {
{/* Tool content */}
-
+ {selectedToolKey && (
+
+ )}
)}
diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx
index d392f21b6..9dec8f45a 100644
--- a/frontend/src/components/tools/ToolPicker.tsx
+++ b/frontend/src/components/tools/ToolPicker.tsx
@@ -1,43 +1,230 @@
-import React from "react";
-import { Box, Text, Stack, Button } from "@mantine/core";
+import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
+import { Box, Text, Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
-import { ToolRegistry } from "../../types/tool";
+import { ToolRegistryEntry } from "../../data/toolsTaxonomy";
+import ToolButton from "./toolPicker/ToolButton";
+import "./toolPicker/ToolPicker.css";
+import { useToolSections } from "../../hooks/useToolSections";
+import SubcategoryHeader from "./shared/SubcategoryHeader";
+import NoToolsFound from "./shared/NoToolsFound";
interface ToolPickerProps {
selectedToolKey: string | null;
onSelect: (id: string) => void;
- /** Pre-filtered tools to display */
- filteredTools: [string, ToolRegistry[string]][];
+ filteredTools: [string, ToolRegistryEntry][];
+ isSearching?: boolean;
}
-const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProps) => {
+// Helper function to render tool buttons for a subcategory
+const renderToolButtons = (
+ subcategory: any,
+ selectedToolKey: string | null,
+ onSelect: (id: string) => void,
+ showSubcategoryHeader: boolean = true
+) => (
+
+ {showSubcategoryHeader && (
+
+ )}
+
+ {subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => (
+
+ ))}
+
+
+);
+
+const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => {
const { t } = useTranslation();
+ const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
+ const [allHeaderHeight, setAllHeaderHeight] = useState(0);
+
+ const scrollableRef = useRef
(null);
+ const quickHeaderRef = useRef(null);
+ const allHeaderRef = useRef(null);
+ const quickAccessRef = useRef(null);
+ const allToolsRef = useRef(null);
+
+ // On resize adjust headers height to offset height
+ useLayoutEffect(() => {
+ const update = () => {
+ if (quickHeaderRef.current) {
+ setQuickHeaderHeight(quickHeaderRef.current.offsetHeight);
+ }
+ if (allHeaderRef.current) {
+ setAllHeaderHeight(allHeaderRef.current.offsetHeight);
+ }
+ };
+ update();
+ window.addEventListener("resize", update);
+ return () => window.removeEventListener("resize", update);
+ }, []);
+
+ const { sections: visibleSections } = useToolSections(filteredTools);
+
+ const quickSection = useMemo(
+ () => visibleSections.find(s => (s as any).key === 'quick'),
+ [visibleSections]
+ );
+ const allSection = useMemo(
+ () => visibleSections.find(s => (s as any).key === 'all'),
+ [visibleSections]
+ );
+
+ const scrollTo = (ref: React.RefObject) => {
+ const container = scrollableRef.current;
+ const target = ref.current;
+ if (container && target) {
+ const stackedOffset = ref === allToolsRef
+ ? (quickHeaderHeight + allHeaderHeight)
+ : quickHeaderHeight;
+ const top = target.offsetTop - container.offsetTop - (stackedOffset || 0);
+ container.scrollTo({
+ top: Math.max(0, top),
+ behavior: "smooth"
+ });
+ }
+ };
+
+ // Build flat list by subcategory for search mode
+ const { searchGroups } = useToolSections(isSearching ? filteredTools : []);
return (
-
-
- {filteredTools.length === 0 ? (
-
- {t("toolPicker.noToolsFound", "No tools found")}
-
+
+
+ {isSearching ? (
+
+ {searchGroups.length === 0 ? (
+
+ ) : (
+ searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect))
+ )}
+
) : (
- filteredTools.map(([id, { icon, name }]) => (
-
+
+ {allSection && (
+ <>
+ scrollTo(allToolsRef)}
+ >
+ {t("toolPicker.allTools", "ALL TOOLS")}
+
+ {allSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
+
+
+
+
+
+ {allSection?.subcategories.map(sc =>
+ renderToolButtons(sc, selectedToolKey, onSelect, true)
+ )}
+
+
+ >
+ )}
+
+ {!quickSection && !allSection && }
+
+ {/* bottom spacer to allow scrolling past the last row */}
+
+ >
+ )}
+
);
};
diff --git a/frontend/src/components/tools/ToolRenderer.tsx b/frontend/src/components/tools/ToolRenderer.tsx
index 493470935..4a3146613 100644
--- a/frontend/src/components/tools/ToolRenderer.tsx
+++ b/frontend/src/components/tools/ToolRenderer.tsx
@@ -26,7 +26,7 @@ const ToolRenderer = ({
// Wrap lazy-loaded component with Suspense
return (
- }>
+ }>
{
- const endpoints = new Set();
- Object.values(EXTENSION_TO_ENDPOINT).forEach(toEndpoints => {
- Object.values(toEndpoints).forEach(endpoint => {
- endpoints.add(endpoint);
- });
- });
- return Array.from(endpoints);
- }, []);
+ const allEndpoints = useMemo(() => getConversionEndpoints(EXTENSION_TO_ENDPOINT), []);
const { endpointStatus } = useMultipleEndpointsEnabled(allEndpoints);
diff --git a/frontend/src/components/tools/shared/NoToolsFound.tsx b/frontend/src/components/tools/shared/NoToolsFound.tsx
new file mode 100644
index 000000000..1b60f0cb7
--- /dev/null
+++ b/frontend/src/components/tools/shared/NoToolsFound.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Text } from '@mantine/core';
+import { useTranslation } from 'react-i18next';
+
+const NoToolsFound: React.FC = () => {
+ const { t } = useTranslation();
+
+ return (
+
+ {t("toolPicker.noToolsFound", "No tools found")}
+
+ );
+};
+
+export default NoToolsFound;
diff --git a/frontend/src/components/tools/shared/SubcategoryHeader.tsx b/frontend/src/components/tools/shared/SubcategoryHeader.tsx
new file mode 100644
index 000000000..b403ebae8
--- /dev/null
+++ b/frontend/src/components/tools/shared/SubcategoryHeader.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+interface SubcategoryHeaderProps {
+ label: string;
+ mt?: string | number;
+ mb?: string | number;
+}
+
+export const SubcategoryHeader: React.FC = ({ label, mt = '1rem', mb = '0.25rem' }) => (
+
+);
+
+export default SubcategoryHeader;
diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx
new file mode 100644
index 000000000..af668a1fa
--- /dev/null
+++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+import { Button } from "@mantine/core";
+import { Tooltip } from "../../shared/Tooltip";
+import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
+import FitText from "../../shared/FitText";
+
+interface ToolButtonProps {
+ id: string;
+ tool: ToolRegistryEntry;
+ isSelected: boolean;
+ onSelect: (id: string) => void;
+}
+
+const ToolButton: React.FC = ({ id, tool, isSelected, onSelect }) => {
+ const handleClick = (id: string) => {
+ if (tool.link) {
+ // Open external link in new tab
+ window.open(tool.link, '_blank', 'noopener,noreferrer');
+ return;
+ }
+ // Normal tool selection
+ onSelect(id);
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default ToolButton;
\ No newline at end of file
diff --git a/frontend/src/components/tools/toolPicker/ToolPicker.css b/frontend/src/components/tools/toolPicker/ToolPicker.css
new file mode 100644
index 000000000..815aee489
--- /dev/null
+++ b/frontend/src/components/tools/toolPicker/ToolPicker.css
@@ -0,0 +1,79 @@
+.tool-picker-scrollable {
+ overflow-y: auto !important;
+ overflow-x: hidden !important;
+ scrollbar-width: thin;
+ scrollbar-color: var(--mantine-color-gray-4) transparent;
+}
+
+.tool-picker-scrollable::-webkit-scrollbar {
+ width: 0.375rem;
+}
+
+.tool-picker-scrollable::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.tool-picker-scrollable::-webkit-scrollbar-thumb {
+ background-color: var(--mantine-color-gray-4);
+ border-radius: 0.1875rem;
+}
+
+.tool-picker-scrollable::-webkit-scrollbar-thumb:hover {
+ background-color: var(--mantine-color-gray-5);
+}
+
+.search-input {
+ margin: 1rem;
+}
+
+.tool-subcategory-title {
+ text-transform: uppercase;
+ padding-bottom: 0.5rem;
+ font-size: 0.75rem;
+ color: var(--tool-subcategory-text-color);
+ /* Align the text with tool labels to account for icon gutter */
+ padding-left: 1rem;
+}
+
+/* New row-style subcategory header with rule */
+.tool-subcategory-row {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.tool-subcategory-row-title {
+ text-transform: uppercase;
+ font-weight: 600;
+ font-size: 0.75rem;
+ color: var(--tool-subcategory-text-color);
+ white-space: nowrap;
+ overflow: visible;
+}
+
+.tool-subcategory-row-rule {
+ height: 1px;
+ background-color: var(--tool-subcategory-rule-color);
+ flex: 1 1 auto;
+}
+
+/* Compact tool buttons */
+.tool-button {
+ font-size: 0.875rem; /* default 1rem - 0.125rem? We'll apply exact -0.25rem via calc below */
+ padding-top: 0.375rem;
+ padding-bottom: 0.375rem;
+}
+
+.tool-button .mantine-Button-label {
+ font-size: .85rem;
+}
+
+.tool-button-icon {
+ font-size: 1rem;
+ line-height: 1;
+}
+
+.search-input-container {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
\ No newline at end of file
diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx
new file mode 100644
index 000000000..f01a9f87d
--- /dev/null
+++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx
@@ -0,0 +1,129 @@
+import React, { useState, useRef, useEffect, useMemo } from "react";
+import { Stack, Button, Text } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
+import { TextInput } from "../../shared/TextInput";
+import './ToolPicker.css';
+
+interface ToolSearchProps {
+ value: string;
+ onChange: (value: string) => void;
+ toolRegistry: Readonly>;
+ onToolSelect?: (toolId: string) => void;
+ mode: 'filter' | 'dropdown';
+ selectedToolKey?: string | null;
+}
+
+const ToolSearch = ({
+ value,
+ onChange,
+ toolRegistry,
+ onToolSelect,
+ mode = 'filter',
+ selectedToolKey
+}: ToolSearchProps) => {
+ const { t } = useTranslation();
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+ const searchRef = useRef(null);
+
+ const filteredTools = useMemo(() => {
+ if (!value.trim()) return [];
+ return Object.entries(toolRegistry)
+ .filter(([id, tool]) => {
+ if (mode === 'dropdown' && id === selectedToolKey) return false;
+ return tool.name.toLowerCase().includes(value.toLowerCase()) ||
+ tool.description.toLowerCase().includes(value.toLowerCase());
+ })
+ .slice(0, 6)
+ .map(([id, tool]) => ({ id, tool }));
+ }, [value, toolRegistry, mode, selectedToolKey]);
+
+ const handleSearchChange = (searchValue: string) => {
+ onChange(searchValue);
+ if (mode === 'dropdown') {
+ setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0);
+ }
+ };
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
+ setDropdownOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const searchInput = (
+
+ search}
+ autoComplete="off"
+ />
+
+ );
+
+ if (mode === 'filter') {
+ return searchInput;
+ }
+
+ return (
+
+ {searchInput}
+ {dropdownOpen && filteredTools.length > 0 && (
+
+
+ {filteredTools.map(({ id, tool }) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default ToolSearch;
\ No newline at end of file
diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx
index d75f2ff5e..637906a22 100644
--- a/frontend/src/contexts/ToolWorkflowContext.tsx
+++ b/frontend/src/contexts/ToolWorkflowContext.tsx
@@ -5,9 +5,8 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
import { useToolManagement } from '../hooks/useToolManagement';
-import { useToolUrlRouting } from '../hooks/useToolUrlRouting';
-import { Tool } from '../types/tool';
import { PageEditorFunctions } from '../types/pageEditor';
+import { ToolRegistryEntry } from '../data/toolsTaxonomy';
// State interface
interface ToolWorkflowState {
@@ -70,9 +69,9 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
interface ToolWorkflowContextValue extends ToolWorkflowState {
// Tool management (from hook)
selectedToolKey: string | null;
- selectedTool: Tool | null;
+ selectedTool: ToolRegistryEntry | null;
toolRegistry: any; // From useToolManagement
-
+
// UI Actions
setSidebarsVisible: (visible: boolean) => void;
setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
@@ -91,7 +90,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
handleReaderToggle: () => void;
// Computed values
- filteredTools: [string, any][]; // Filtered by search
+ filteredTools: [string, ToolRegistryEntry][]; // Filtered by search
isPanelVisible: boolean;
}
@@ -143,11 +142,22 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: string) => {
+ // Special-case: if tool is a dedicated reader tool, enter reader mode and do not go to toolContent
+ if (toolId === 'read' || toolId === 'view-pdf') {
+ setReaderMode(true);
+ setLeftPanelView('toolPicker');
+ clearToolSelection();
+ setSearchQuery('');
+ return;
+ }
+
selectTool(toolId);
onViewChange?.('fileEditor');
setLeftPanelView('toolContent');
setReaderMode(false);
- }, [selectTool, onViewChange, setLeftPanelView, setReaderMode]);
+ // Clear search so the tool content becomes visible immediately
+ setSearchQuery('');
+ }, [selectTool, onViewChange, setLeftPanelView, setReaderMode, setSearchQuery, clearToolSelection]);
const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker');
@@ -159,20 +169,6 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro
setReaderMode(true);
}, [setReaderMode]);
- // URL routing functionality
- const { getToolUrlSlug, getToolKeyFromSlug } = useToolUrlRouting({
- selectedToolKey,
- toolRegistry,
- selectTool,
- clearToolSelection,
- // During initial load, we want the full UI side-effects (like before):
- onInitSelect: handleToolSelect,
- // For back/forward nav, keep it lightweight like before (selection only):
- onPopStateSelect: selectTool,
- // If your app serves under a subpath, provide basePath here (e.g., '/app')
- // basePath: ''
- });
-
// Filter tools based on search query
const filteredTools = useMemo(() => {
if (!toolRegistry) return [];
@@ -228,9 +224,4 @@ export function useToolWorkflow(): ToolWorkflowContextValue {
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
}
return context;
-}
-
-// Convenience exports for specific use cases (optional - components can use useToolWorkflow directly)
-export const useToolSelection = useToolWorkflow;
-export const useToolPanelState = useToolWorkflow;
-export const useWorkbenchState = useToolWorkflow;
+}
\ No newline at end of file
diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts
new file mode 100644
index 000000000..cb6b18c0d
--- /dev/null
+++ b/frontend/src/data/toolsTaxonomy.ts
@@ -0,0 +1,102 @@
+import { type TFunction } from 'i18next';
+import React from 'react';
+
+export enum SubcategoryId {
+ SIGNING = 'signing',
+ DOCUMENT_SECURITY = 'documentSecurity',
+ VERIFICATION = 'verification',
+ DOCUMENT_REVIEW = 'documentReview',
+ PAGE_FORMATTING = 'pageFormatting',
+ EXTRACTION = 'extraction',
+ REMOVAL = 'removal',
+ AUTOMATION = 'automation',
+ GENERAL = 'general',
+ ADVANCED_FORMATTING = 'advancedFormatting',
+ DEVELOPER_TOOLS = 'developerTools'
+}
+
+export enum ToolCategory {
+ STANDARD_TOOLS = 'Standard Tools',
+ ADVANCED_TOOLS = 'Advanced Tools',
+ RECOMMENDED_TOOLS = 'Recommended Tools'
+}
+
+export type ToolRegistryEntry = {
+ icon: React.ReactNode;
+ name: string;
+ component: React.ComponentType | null;
+ view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
+ description: string;
+ category: ToolCategory;
+ subcategory: SubcategoryId;
+ maxFiles?: number;
+ supportedFormats?: string[];
+ endpoints?: string[];
+ link?: string;
+ type?: string;
+}
+
+export type ToolRegistry = Record;
+
+export const SUBCATEGORY_ORDER: SubcategoryId[] = [
+ SubcategoryId.SIGNING,
+ SubcategoryId.DOCUMENT_SECURITY,
+ SubcategoryId.VERIFICATION,
+ SubcategoryId.DOCUMENT_REVIEW,
+ SubcategoryId.PAGE_FORMATTING,
+ SubcategoryId.EXTRACTION,
+ SubcategoryId.REMOVAL,
+ SubcategoryId.AUTOMATION,
+ SubcategoryId.GENERAL,
+ SubcategoryId.ADVANCED_FORMATTING,
+ SubcategoryId.DEVELOPER_TOOLS,
+];
+
+export const SUBCATEGORY_COLOR_MAP: Record = {
+ [SubcategoryId.SIGNING]: '#FF7892',
+ [SubcategoryId.DOCUMENT_SECURITY]: '#FF7892',
+ [SubcategoryId.VERIFICATION]: '#1BB1D4',
+ [SubcategoryId.DOCUMENT_REVIEW]: '#48BD54',
+ [SubcategoryId.PAGE_FORMATTING]: '#7882FF',
+ [SubcategoryId.EXTRACTION]: '#1BB1D4',
+ [SubcategoryId.REMOVAL]: '#7882FF',
+ [SubcategoryId.AUTOMATION]: '#69DC95',
+ [SubcategoryId.GENERAL]: '#69DC95',
+ [SubcategoryId.ADVANCED_FORMATTING]: '#F55454',
+ [SubcategoryId.DEVELOPER_TOOLS]: '#F55454',
+};
+
+export const getSubcategoryColor = (subcategory: SubcategoryId): string => SUBCATEGORY_COLOR_MAP[subcategory] || '#7882FF';
+
+export const getSubcategoryLabel = (t: TFunction, id: SubcategoryId): string => t(`toolPicker.subcategories.${id}`, id);
+
+
+
+export const getAllEndpoints = (registry: ToolRegistry): string[] => {
+ const lists: string[][] = [];
+ Object.values(registry).forEach(entry => {
+ if (entry.endpoints && entry.endpoints.length > 0) {
+ lists.push(entry.endpoints);
+ }
+ });
+ return Array.from(new Set(lists.flat()));
+};
+
+export const getConversionEndpoints = (extensionToEndpoint: Record>): string[] => {
+ const endpoints = new Set();
+ Object.values(extensionToEndpoint).forEach(toEndpoints => {
+ Object.values(toEndpoints).forEach(endpoint => {
+ endpoints.add(endpoint);
+ });
+ });
+ return Array.from(endpoints);
+};
+
+export const getAllApplicationEndpoints = (
+ registry: ToolRegistry,
+ extensionToEndpoint?: Record>
+): string[] => {
+ const toolEp = getAllEndpoints(registry);
+ const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : [];
+ return Array.from(new Set([...toolEp, ...convEp]));
+};
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
new file mode 100644
index 000000000..5f6460217
--- /dev/null
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -0,0 +1,606 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import SplitPdfPanel from "../tools/Split";
+import CompressPdfPanel from "../tools/Compress";
+import OCRPanel from '../tools/OCR';
+import ConvertPanel from '../tools/Convert';
+import Sanitize from '../tools/Sanitize';
+import AddPassword from '../tools/AddPassword';
+import ChangePermissions from '../tools/ChangePermissions';
+import RemovePassword from '../tools/RemovePassword';
+import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy';
+import AddWatermark from '../tools/AddWatermark';
+
+// Hook to get the translated tool registry
+export function useFlatToolRegistry(): ToolRegistry {
+ const { t } = useTranslation();
+
+ return {
+ // Signing
+
+ "certSign": {
+ icon: workspace_premium,
+ name: t("home.certSign.title", "Sign with Certificate"),
+ component: null,
+ view: "sign",
+ description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.SIGNING
+ },
+ "sign": {
+ icon: signature,
+ name: t("home.sign.title", "Sign"),
+ component: null,
+ view: "sign",
+ description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.SIGNING
+ },
+
+
+ // Document Security
+
+ "addPassword": {
+ icon: password,
+ name: t("home.addPassword.title", "Add Password"),
+ component: AddPassword,
+ view: "security",
+ description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY,
+ maxFiles: -1,
+ endpoints: ["add-password"]
+ },
+ "add-watermark": {
+ icon: branding_watermark,
+ name: t("home.watermark.title", "Add Watermark"),
+ component: AddWatermark,
+ view: "format",
+ maxFiles: -1,
+ description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY,
+ endpoints: ["add-watermark"]
+ },
+ "add-stamp": {
+ icon: approval,
+ name: t("home.AddStampRequest.title", "Add Stamp to PDF"),
+ component: null,
+ view: "format",
+ description: t("home.AddStampRequest.desc", "Add text or add image stamps at set locations"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY
+ },
+ "sanitize": {
+ icon: cleaning_services,
+ name: t("home.sanitize.title", "Sanitize"),
+ component: Sanitize,
+ view: "security",
+ maxFiles: -1,
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY,
+ description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
+ endpoints: ["sanitize-pdf"]
+ },
+ "flatten": {
+ icon: layers_clear,
+ name: t("home.flatten.title", "Flatten"),
+ component: null,
+ view: "format",
+ description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY
+ },
+ "unlock-pdf-forms": {
+ icon: preview_off,
+ name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
+ component: null,
+ view: "security",
+ description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY
+ },
+ "manage-certificates": {
+ icon: license,
+ name: t("home.manageCertificates.title", "Manage Certificates"),
+ component: null,
+ view: "security",
+ description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY
+ },
+ "change-permissions": {
+ icon: lock,
+ name: t("home.changePermissions.title", "Change Permissions"),
+ component: ChangePermissions,
+ view: "security",
+ description: t("home.changePermissions.desc", "Change document restrictions and permissions"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_SECURITY,
+ maxFiles: -1,
+ endpoints: ["add-password"]
+ },
+ // Verification
+
+ "get-all-info-on-pdf": {
+ icon: fact_check,
+ name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
+ component: null,
+ view: "extract",
+ description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.VERIFICATION
+ },
+ "validate-pdf-signature": {
+ icon: verified,
+ name: t("home.validateSignature.title", "Validate PDF Signature"),
+ component: null,
+ view: "security",
+ description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.VERIFICATION
+ },
+
+
+ // Document Review
+
+ "read": {
+ icon: article,
+ name: t("home.read.title", "Read"),
+ component: null,
+ view: "view",
+ description: t("home.read.desc", "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_REVIEW
+ },
+ "change-metadata": {
+ icon: assignment,
+ name: t("home.changeMetadata.title", "Change Metadata"),
+ component: null,
+ view: "format",
+ description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.DOCUMENT_REVIEW
+ },
+ // Page Formatting
+
+ "cropPdf": {
+ icon: crop,
+ name: t("home.crop.title", "Crop PDF"),
+ component: null,
+ view: "format",
+ description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "rotate": {
+ icon: rotate_right,
+ name: t("home.rotate.title", "Rotate"),
+ component: null,
+ view: "format",
+ description: t("home.rotate.desc", "Easily rotate your PDFs."),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "splitPdf": {
+ icon: content_cut,
+ name: t("home.split.title", "Split"),
+ component: SplitPdfPanel,
+ view: "split",
+ description: t("home.split.desc", "Split PDFs into multiple documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "reorganize-pages": {
+ icon: move_down,
+ name: t("home.reorganizePages.title", "Reorganize Pages"),
+ component: null,
+ view: "pageEditor",
+ description: t("home.reorganizePages.desc", "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "adjust-page-size-scale": {
+ icon: crop_free,
+ name: t("home.scalePages.title", "Adjust page size/scale"),
+ component: null,
+ view: "format",
+ description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "add-page-numbers": {
+ icon: 123,
+ name: t("home.add-page-numbers.title", "Add Page Numbers"),
+ component: null,
+ view: "format",
+ description: t("home.add-page-numbers.desc", "Add Page numbers throughout a document in a set location"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "multi-page-layout": {
+ icon: dashboard,
+ name: t("home.pageLayout.title", "Multi-Page Layout"),
+ component: null,
+ view: "format",
+ description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "single-large-page": {
+ icon: looks_one,
+ name: t("home.PdfToSinglePage.title", "PDF to Single Large Page"),
+ component: null,
+ view: "format",
+ description: t("home.PdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING
+ },
+ "add-attachments": {
+ icon: attachment,
+ name: t("home.attachments.title", "Add Attachments"),
+ component: null,
+ view: "format",
+ description: t("home.attachments.desc", "Add or remove embedded files (attachments) to/from a PDF"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.PAGE_FORMATTING,
+ },
+
+
+ // Extraction
+
+ "extract-pages": {
+ icon: upload,
+ name: t("home.extractPage.title", "Extract Pages"),
+ component: null,
+ view: "extract",
+ description: t("home.extractPage.desc", "Extract specific pages from a PDF document"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.EXTRACTION
+ },
+ "extract-images": {
+ icon: filter,
+ name: t("home.extractImages.title", "Extract Images"),
+ component: null,
+ view: "extract",
+ description: t("home.extractImages.desc", "Extract images from PDF documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.EXTRACTION
+ },
+
+
+ // Removal
+
+ "remove": {
+ icon: delete,
+ name: t("home.removePages.title", "Remove Pages"),
+ component: null,
+ view: "remove",
+ description: t("home.removePages.desc", "Remove specific pages from a PDF document"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.REMOVAL
+ },
+ "remove-blank-pages": {
+ icon: scan_delete,
+ name: t("home.removeBlanks.title", "Remove Blank Pages"),
+ component: null,
+ view: "remove",
+ description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.REMOVAL
+ },
+ "remove-annotations": {
+ icon: thread_unread,
+ name: t("home.removeAnnotations.title", "Remove Annotations"),
+ component: null,
+ view: "remove",
+ description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.REMOVAL
+ },
+ "remove-image": {
+ icon: remove_selection,
+ name: t("home.removeImagePdf.title", "Remove Image"),
+ component: null,
+ view: "format",
+ description: t("home.removeImagePdf.desc", "Remove images from PDF documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.REMOVAL
+ },
+ "remove-password": {
+ icon: lock_open_right,
+ name: t("home.removePassword.title", "Remove Password"),
+ component: RemovePassword,
+ view: "security",
+ description: t("home.removePassword.desc", "Remove password protection from PDF documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.REMOVAL,
+ endpoints: ["remove-password"],
+ maxFiles: -1,
+
+ },
+ "remove-certificate-sign": {
+ icon: remove_moderator,
+ name: t("home.removeCertSign.title", "Remove Certificate Signatures"),
+ component: null,
+ view: "security",
+ description: t("home.removeCertSign.desc", "Remove digital signatures from PDF documents"),
+ category: ToolCategory.STANDARD_TOOLS,
+ subcategory: SubcategoryId.REMOVAL
+ },
+
+
+ // Automation
+
+ "automate": {
+ icon: automation,
+ name: t("home.automate.title", "Automate"),
+ component: null,
+ view: "format",
+ description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.AUTOMATION
+ },
+ "auto-rename-pdf-file": {
+ icon: match_word,
+ name: t("home.auto-rename.title", "Auto Rename PDF File"),
+ component: null,
+ view: "format",
+ description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.AUTOMATION
+ },
+ "auto-split-pages": {
+ icon: split_scene_right,
+ name: t("home.autoSplitPDF.title", "Auto Split Pages"),
+ component: null,
+ view: "format",
+ description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.AUTOMATION
+ },
+ "auto-split-by-size-count": {
+ icon: content_cut,
+ name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"),
+ component: null,
+ view: "format",
+ description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.AUTOMATION
+ },
+
+
+ // Advanced Formatting
+
+ "adjust-colors-contrast": {
+ icon: palette,
+ name: t("home.adjust-contrast.title", "Adjust Colors/Contrast"),
+ component: null,
+ view: "format",
+ description: t("home.adjust-contrast.desc", "Adjust colors and contrast of PDF documents"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+ "repair": {
+ icon: build,
+ name: t("home.repair.title", "Repair"),
+ component: null,
+ view: "format",
+ description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+ "detect-split-scanned-photos": {
+ icon: scanner,
+ name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"),
+ component: null,
+ view: "format",
+ description: t("home.ScannerImageSplit.desc", "Detect and split scanned photos into separate pages"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+ "overlay-pdfs": {
+ icon: layers,
+ name: t("home.overlay-pdfs.title", "Overlay PDFs"),
+ component: null,
+ view: "format",
+ description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+ "replace-and-invert-color": {
+ icon: format_color_fill,
+ name: t("home.replaceColorPdf.title", "Replace & Invert Color"),
+ component: null,
+ view: "format",
+ description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+ "add-image": {
+ icon: image,
+ name: t("home.addImage.title", "Add Image"),
+ component: null,
+ view: "format",
+ description: t("home.addImage.desc", "Add images to PDF documents"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+ "edit-table-of-contents": {
+ icon: bookmark_add,
+ name: t("home.editTableOfContents.title", "Edit Table of Contents"),
+ component: null,
+ view: "format",
+ description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+ "scanner-effect": {
+ icon: scanner,
+ name: t("home.fakeScan.title", "Scanner Effect"),
+ component: null,
+ view: "format",
+ description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.ADVANCED_FORMATTING
+ },
+
+
+ // Developer Tools
+
+ "show-javascript": {
+ icon: javascript,
+ name: t("home.showJS.title", "Show JavaScript"),
+ component: null,
+ view: "extract",
+ description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.DEVELOPER_TOOLS
+ },
+ "dev-api": {
+ icon: open_in_new,
+ name: t("home.devApi.title", "API"),
+ component: null,
+ view: "external",
+ description: t("home.devApi.desc", "Link to API documentation"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.DEVELOPER_TOOLS,
+ link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html"
+ },
+ "dev-folder-scanning": {
+ icon: open_in_new,
+ name: t("home.devFolderScanning.title", "Automated Folder Scanning"),
+ component: null,
+ view: "external",
+ description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.DEVELOPER_TOOLS,
+ link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/"
+ },
+ "dev-sso-guide": {
+ icon: open_in_new,
+ name: t("home.devSsoGuide.title", "SSO Guide"),
+ component: null,
+ view: "external",
+ description: t("home.devSsoGuide.desc", "Link to SSO guide"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.DEVELOPER_TOOLS,
+ link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
+ },
+ "dev-airgapped": {
+ icon: open_in_new,
+ name: t("home.devAirgapped.title", "Air-gapped Setup"),
+ component: null,
+ view: "external",
+ description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"),
+ category: ToolCategory.ADVANCED_TOOLS,
+ subcategory: SubcategoryId.DEVELOPER_TOOLS,
+ link: "https://docs.stirlingpdf.com/Pro/#activation"
+ },
+
+
+ // Recommended Tools
+ "compare": {
+ icon: compare,
+ name: t("home.compare.title", "Compare"),
+ component: null,
+ view: "format",
+ description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
+ category: ToolCategory.RECOMMENDED_TOOLS,
+ subcategory: SubcategoryId.GENERAL
+ },
+ "compressPdfs": {
+ icon: zoom_in_map,
+ name: t("home.compressPdfs.title", "Compress"),
+ component: CompressPdfPanel,
+ view: "compress",
+ description: t("home.compressPdfs.desc", "Compress PDFs to reduce their file size."),
+ category: ToolCategory.RECOMMENDED_TOOLS,
+ subcategory: SubcategoryId.GENERAL,
+ maxFiles: -1
+ },
+ "convert": {
+ icon: sync_alt,
+ name: t("home.fileToPDF.title", "Convert"),
+ component: ConvertPanel,
+ view: "convert",
+ description: t("home.fileToPDF.desc", "Convert files to and from PDF format"),
+ category: ToolCategory.RECOMMENDED_TOOLS,
+ subcategory: SubcategoryId.GENERAL,
+ maxFiles: -1,
+ endpoints: [
+ "pdf-to-img",
+ "img-to-pdf",
+ "pdf-to-word",
+ "pdf-to-presentation",
+ "pdf-to-text",
+ "pdf-to-html",
+ "pdf-to-xml",
+ "html-to-pdf",
+ "markdown-to-pdf",
+ "file-to-pdf",
+ "pdf-to-csv",
+ "pdf-to-markdown",
+ "pdf-to-pdfa",
+ "eml-to-pdf"
+ ],
+ supportedFormats: [
+ // Microsoft Office
+ "doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
+ // OpenDocument
+ "odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
+ // Text formats
+ "txt", "text", "xml", "rtf", "html", "lwp", "md",
+ // Images
+ "bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
+ // StarOffice
+ "sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
+ // Email formats
+ "eml",
+ // Archive formats
+ "zip",
+ // Other
+ "dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
+ ]
+ },
+ "mergePdfs": {
+ icon: library_add,
+ name: t("home.merge.title", "Merge"),
+ component: null,
+ view: "merge",
+ description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
+ category: ToolCategory.RECOMMENDED_TOOLS,
+ subcategory: SubcategoryId.GENERAL,
+ maxFiles: -1
+ },
+ "multi-tool": {
+ icon: dashboard_customize,
+ name: t("home.multiTool.title", "Multi-Tool"),
+ component: null,
+ view: "pageEditor",
+ description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
+ category: ToolCategory.RECOMMENDED_TOOLS,
+ subcategory: SubcategoryId.GENERAL,
+ maxFiles: -1
+ },
+ "ocr": {
+ icon: quick_reference_all,
+ name: t("home.ocr.title", "OCR"),
+ component: OCRPanel,
+ view: "convert",
+ description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"),
+ category: ToolCategory.RECOMMENDED_TOOLS,
+ subcategory: SubcategoryId.GENERAL,
+ maxFiles: -1
+ },
+ "redact": {
+ icon: visibility_off,
+ name: t("home.redact.title", "Redact"),
+ component: null,
+ view: "redact",
+ description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"),
+ category: ToolCategory.RECOMMENDED_TOOLS,
+ subcategory: SubcategoryId.GENERAL
+ },
+ };
+}
\ No newline at end of file
diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts
index efbaa35a9..718bc9370 100644
--- a/frontend/src/global.d.ts
+++ b/frontend/src/global.d.ts
@@ -4,4 +4,5 @@ declare module "../tools/Merge";
declare module "../components/PageEditor";
declare module "../components/Viewer";
declare module "*.js";
-declare module '*.module.css';
\ No newline at end of file
+declare module '*.module.css';
+declare module 'pdfjs-dist';
\ No newline at end of file
diff --git a/frontend/src/hooks/usePdfSignatureDetection.ts b/frontend/src/hooks/usePdfSignatureDetection.ts
index 96d80fb9d..ea7d0bdf0 100644
--- a/frontend/src/hooks/usePdfSignatureDetection.ts
+++ b/frontend/src/hooks/usePdfSignatureDetection.ts
@@ -34,7 +34,7 @@ export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionRe
const page = await pdf.getPage(i);
const annotations = await page.getAnnotations({ intent: 'display' });
- annotations.forEach(annotation => {
+ annotations.forEach((annotation: any) => {
if (annotation.subtype === 'Widget' && annotation.fieldType === 'Sig') {
foundSignature = true;
}
diff --git a/frontend/src/hooks/useRainbowTheme.ts b/frontend/src/hooks/useRainbowTheme.ts
index a3c8f6e67..b16ed1228 100644
--- a/frontend/src/hooks/useRainbowTheme.ts
+++ b/frontend/src/hooks/useRainbowTheme.ts
@@ -18,7 +18,13 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
if (stored && ['light', 'dark', 'rainbow'].includes(stored)) {
return stored as ThemeMode;
}
- return initialTheme;
+ try {
+ // Fallback to OS preference if available
+ const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+ return prefersDark ? 'dark' : initialTheme;
+ } catch {
+ return initialTheme;
+ }
});
// Track rapid toggles for easter egg
diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx
index 5ac19d74e..c19e5b6af 100644
--- a/frontend/src/hooks/useToolManagement.tsx
+++ b/frontend/src/hooks/useToolManagement.tsx
@@ -1,137 +1,14 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import ContentCutIcon from "@mui/icons-material/ContentCut";
-import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
-import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
-import ApiIcon from "@mui/icons-material/Api";
-import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
-import LockIcon from "@mui/icons-material/Lock";
-import BrandingWatermarkIcon from "@mui/icons-material/BrandingWatermark";
-import LockOpenIcon from "@mui/icons-material/LockOpen";
+import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
+import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
-import { Tool, ToolDefinition, ToolRegistry } from "../types/tool";
-
-
-// Add entry here with maxFiles, endpoints, and lazy component
-const toolDefinitions: Record = {
- split: {
- id: "split",
- icon: ,
- component: React.lazy(() => import("../tools/Split")),
- maxFiles: 1,
- category: "manipulation",
- description: "Split PDF files into smaller parts",
- endpoints: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"]
- },
- compress: {
- id: "compress",
- icon: ,
- component: React.lazy(() => import("../tools/Compress")),
- maxFiles: -1,
- category: "optimization",
- description: "Reduce PDF file size",
- endpoints: ["compress-pdf"]
- },
- convert: {
- id: "convert",
- icon: ,
- component: React.lazy(() => import("../tools/Convert")),
- maxFiles: -1,
- category: "manipulation",
- description: "Change to and from PDF and other formats",
- endpoints: ["pdf-to-img", "img-to-pdf", "pdf-to-word", "pdf-to-presentation", "pdf-to-text", "pdf-to-html", "pdf-to-xml", "html-to-pdf", "markdown-to-pdf", "file-to-pdf"],
- supportedFormats: [
- // Microsoft Office
- "doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
- // OpenDocument
- "odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
- // Text formats
- "txt", "text", "xml", "rtf", "html", "lwp", "md",
- // Images
- "bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
- // StarOffice
- "sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
- // Email formats
- "eml",
- // Archive formats
- "zip",
- // Other
- "dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
- ]
- },
- swagger: {
- id: "swagger",
- icon: ,
- component: React.lazy(() => import("../tools/SwaggerUI")),
- maxFiles: 0,
- category: "utility",
- description: "Open API documentation",
- endpoints: ["swagger-ui"]
- },
- ocr: {
- id: "ocr",
- icon:
- quick_reference_all
- ,
- component: React.lazy(() => import("../tools/OCR")),
- maxFiles: -1,
- category: "utility",
- description: "Extract text from images using OCR",
- endpoints: ["ocr-pdf"]
- },
- sanitize: {
- id: "sanitize",
- icon: ,
- component: React.lazy(() => import("../tools/Sanitize")),
- maxFiles: -1,
- category: "security",
- description: "Remove potentially harmful elements from PDF files",
- endpoints: ["sanitize-pdf"]
- },
- addPassword: {
- id: "addPassword",
- icon: ,
- component: React.lazy(() => import("../tools/AddPassword")),
- maxFiles: -1,
- category: "security",
- description: "Add password protection and restrictions to PDF files",
- endpoints: ["add-password"]
- },
- changePermissions: {
- id: "changePermissions",
- icon: ,
- component: React.lazy(() => import("../tools/ChangePermissions")),
- maxFiles: -1,
- category: "security",
- description: "Change document restrictions and permissions",
- endpoints: ["add-password"]
- },
- watermark: {
- id: "watermark",
- icon: ,
- component: React.lazy(() => import("../tools/AddWatermark")),
- maxFiles: -1,
- category: "security",
- description: "Add text or image watermarks to PDF files",
- endpoints: ["add-watermark"]
- },
- removePassword: {
- id: "removePassword",
- icon: ,
- component: React.lazy(() => import("../tools/RemovePassword")),
- maxFiles: -1,
- category: "security",
- description: "Remove password protection from PDF files",
- endpoints: ["remove-password"]
- },
-
-};
interface ToolManagementResult {
selectedToolKey: string | null;
- selectedTool: Tool | null;
+ selectedTool: ToolRegistryEntry | null;
toolSelectedFileIds: string[];
- toolRegistry: ToolRegistry;
+ toolRegistry: Record;
selectTool: (toolKey: string) => void;
clearToolSelection: () => void;
setToolSelectedFileIds: (fileIds: string[]) => void;
@@ -143,33 +20,41 @@ export const useToolManagement = (): ToolManagementResult => {
const [selectedToolKey, setSelectedToolKey] = useState(null);
const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]);
- const allEndpoints = Array.from(new Set(
- Object.values(toolDefinitions).flatMap(tool => tool.endpoints || [])
- ));
+ // Build endpoints list from registry entries with fallback to legacy mapping
+ const baseRegistry = useFlatToolRegistry();
+ const registryDerivedEndpoints = useMemo(() => {
+ const endpointsByTool: Record = {};
+ Object.entries(baseRegistry).forEach(([key, entry]) => {
+ if (entry.endpoints && entry.endpoints.length > 0) {
+ endpointsByTool[key] = entry.endpoints;
+ }
+ });
+ return endpointsByTool;
+ }, [baseRegistry]);
+
+ const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
const isToolAvailable = useCallback((toolKey: string): boolean => {
if (endpointsLoading) return true;
- const tool = toolDefinitions[toolKey];
- if (!tool?.endpoints) return true;
- return tool.endpoints.some(endpoint => endpointStatus[endpoint] === true);
- }, [endpointsLoading, endpointStatus]);
+ const endpoints = baseRegistry[toolKey]?.endpoints || [];
+ return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
+ }, [endpointsLoading, endpointStatus, baseRegistry]);
- const toolRegistry: ToolRegistry = useMemo(() => {
- const availableTools: ToolRegistry = {};
- Object.keys(toolDefinitions).forEach(toolKey => {
+ const toolRegistry: Record = useMemo(() => {
+ const availableToolRegistry: Record = {};
+ Object.keys(baseRegistry).forEach(toolKey => {
if (isToolAvailable(toolKey)) {
- const toolDef = toolDefinitions[toolKey];
- availableTools[toolKey] = {
- ...toolDef,
- name: t(`${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1)),
- title: t(`${toolKey}.title`, toolDef.description || toolKey),
- description: t(`${toolKey}.desc`, toolDef.description || `${toolKey} tool`)
+ const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
+ availableToolRegistry[toolKey] = {
+ ...baseTool,
+ name: t(baseTool.name),
+ description: t(baseTool.description)
};
}
});
- return availableTools;
- }, [t, isToolAvailable]);
+ return availableToolRegistry;
+ }, [isToolAvailable, t, baseRegistry]);
useEffect(() => {
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) {
@@ -197,7 +82,6 @@ export const useToolManagement = (): ToolManagementResult => {
selectedTool,
toolSelectedFileIds,
toolRegistry,
-
selectTool,
clearToolSelection,
setToolSelectedFileIds,
diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts
new file mode 100644
index 000000000..4c1a0c05d
--- /dev/null
+++ b/frontend/src/hooks/useToolSections.ts
@@ -0,0 +1,88 @@
+import { useMemo } from 'react';
+
+import { SUBCATEGORY_ORDER, ToolCategory, ToolRegistryEntry } from '../data/toolsTaxonomy';
+import { useTranslation } from 'react-i18next';
+
+type GroupedTools = {
+ [category: string]: {
+ [subcategory: string]: Array<{ id: string; tool: ToolRegistryEntry }>;
+ };
+};
+
+export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) {
+ const { t } = useTranslation();
+
+ const groupedTools = useMemo(() => {
+ const grouped: GroupedTools = {};
+ filteredTools.forEach(([id, tool]) => {
+ const category = tool.category;
+ const subcategory = tool.subcategory;
+ if (!grouped[category]) grouped[category] = {};
+ if (!grouped[category][subcategory]) grouped[category][subcategory] = [];
+ grouped[category][subcategory].push({ id, tool });
+ });
+ return grouped;
+ }, [filteredTools]);
+
+ const sections = useMemo(() => {
+ const getOrderIndex = (name: string) => {
+ const idx = SUBCATEGORY_ORDER.indexOf(name as any);
+ return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
+ };
+
+ const quick: Record> = {};
+ const all: Record> = {};
+
+ Object.entries(groupedTools).forEach(([origCat, subs]) => {
+ const upperCat = origCat.toUpperCase();
+
+ Object.entries(subs).forEach(([sub, tools]) => {
+ if (!all[sub]) all[sub] = [];
+ all[sub].push(...tools);
+ });
+
+ if (upperCat === ToolCategory.RECOMMENDED_TOOLS.toUpperCase()) {
+ Object.entries(subs).forEach(([sub, tools]) => {
+ if (!quick[sub]) quick[sub] = [];
+ quick[sub].push(...tools);
+ });
+ }
+ });
+
+ const sortSubs = (obj: Record>) =>
+ Object.entries(obj)
+ .sort(([a], [b]) => {
+ const ai = getOrderIndex(a);
+ const bi = getOrderIndex(b);
+ if (ai !== bi) return ai - bi;
+ return a.localeCompare(b);
+ })
+ .map(([subcategory, tools]) => ({ subcategory, tools }));
+
+ const built = [
+ { key: 'quick', title: t('toolPicker.quickAccess', 'QUICK ACCESS'), subcategories: sortSubs(quick) },
+ { key: 'all', title: t('toolPicker.allTools', 'ALL TOOLS'), subcategories: sortSubs(all) }
+ ];
+
+ return built.filter(section => section.subcategories.some(sc => sc.tools.length > 0));
+ }, [groupedTools]);
+
+ const searchGroups = useMemo(() => {
+ const subMap: Record> = {};
+ const seen = new Set();
+ filteredTools.forEach(([id, tool]) => {
+ if (seen.has(id)) return;
+ seen.add(id);
+ const sub = tool.subcategory;
+ if (!subMap[sub]) subMap[sub] = [];
+ subMap[sub].push({ id, tool });
+ });
+ return Object.entries(subMap)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([subcategory, tools]) => ({ subcategory, tools }));
+ }, [filteredTools]);
+
+ return { sections, searchGroups };
+}
+
+
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index e8d719e24..740eec3dc 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -2,11 +2,22 @@ import '@mantine/core/styles.css';
import './index.css'; // Import Tailwind CSS
import React from 'react';
import ReactDOM from 'react-dom/client';
-import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/core';
+import { ColorSchemeScript } from '@mantine/core';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './i18n'; // Initialize i18next
+// Compute initial color scheme
+function getInitialScheme(): 'light' | 'dark' {
+ const stored = localStorage.getItem('stirling-theme');
+ if (stored === 'light' || stored === 'dark') return stored;
+ try {
+ const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+ return prefersDark ? 'dark' : 'light';
+ } catch {
+ return 'light';
+ }
+}
const container = document.getElementById('root');
if (!container) {
@@ -15,12 +26,10 @@ if (!container) {
const root = ReactDOM.createRoot(container); // Finds the root DOM element
root.render(
-
-
-
-
-
-
+
+
+
+
);
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index a931f0090..d26a40caa 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -2,7 +2,7 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useFileContext } from "../contexts/FileContext";
import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext";
-import { ToolWorkflowProvider, useToolSelection } from "../contexts/ToolWorkflowContext";
+import { ToolWorkflowProvider, useToolWorkflow } from "../contexts/ToolWorkflowContext";
import { Group } from "@mantine/core";
import { SidebarProvider, useSidebarContext } from "../contexts/SidebarContext";
import { useDocumentMeta } from "../hooks/useDocumentMeta";
@@ -24,24 +24,24 @@ function HomePageContent() {
const { setMaxFiles, setIsToolMode, setSelectedFiles } = useFileSelection();
- const { selectedTool } = useToolSelection();
+ const { selectedTool, selectedToolKey } = useToolWorkflow();
const baseUrl = getBaseUrl();
// Update document meta when tool changes
useDocumentMeta({
- title: selectedTool?.title ? `${selectedTool.title} - Stirling PDF` : 'Stirling PDF',
+ title: selectedTool ? `${selectedTool.name} - Stirling PDF` : 'Stirling PDF',
description: selectedTool?.description || t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'),
- ogTitle: selectedTool?.title ? `${selectedTool.title} - Stirling PDF` : 'Stirling PDF',
+ ogTitle: selectedTool ? `${selectedTool.name} - Stirling PDF` : 'Stirling PDF',
ogDescription: selectedTool?.description || t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'),
- ogImage: selectedTool ? `${baseUrl}/og_images/${selectedTool.id}.png` : `${baseUrl}/og_images/home.png`,
+ ogImage: selectedToolKey ? `${baseUrl}/og_images/${selectedToolKey}.png` : `${baseUrl}/og_images/home.png`,
ogUrl: selectedTool ? `${baseUrl}${window.location.pathname}` : baseUrl
});
// Update file selection context when tool changes
useEffect(() => {
if (selectedTool) {
- setMaxFiles(selectedTool.maxFiles);
+ setMaxFiles(selectedTool.maxFiles ?? -1);
setIsToolMode(true);
} else {
setMaxFiles(-1);
@@ -76,4 +76,4 @@ export default function HomePage() {
);
-}
+}
\ No newline at end of file
diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css
index 9ec48bca7..634cae91c 100644
--- a/frontend/src/styles/theme.css
+++ b/frontend/src/styles/theme.css
@@ -81,7 +81,7 @@
--text-secondary: #4b5563;
--text-muted: #6b7280;
--border-subtle: #e5e7eb;
- --border-default: #d1d5db;
+ --border-default: #E2E8F0;
--border-strong: #9ca3af;
--hover-bg: #f9fafb;
--active-bg: #f3f4f6;
@@ -117,11 +117,31 @@
--icon-inactive-bg: #9CA3AF;
--icon-inactive-color: #FFFFFF;
+ /* New theme colors for text and icons */
+ --tools-text-and-icon-color: #374151;
+
+ /* Tool picker sticky header variables (light mode) */
+ --tool-header-bg: #DBEFFF;
+ --tool-header-border: #BEE2FF;
+ --tool-header-text: #1E88E5;
+ --tool-header-badge-bg: #C0DDFF;
+ --tool-header-badge-text: #004E99;
+
+ /* Subcategory title styling (light mode) */
+ --tool-subcategory-text-color: #9CA3AF; /* lighter text */
+ --tool-subcategory-rule-color: #E5E7EB; /* doubly lighter rule line */
--accent-interactive: #4A90E2;
--text-instruction: #4A90E2;
--text-brand: var(--color-gray-700);
--text-brand-accent: #DC2626;
+ /* Placeholder text colors */
+ --search-text-and-icon-color: #6B7382;
+
+ /* Tool panel search bar background colors */
+ --tool-panel-search-bg: #EFF1F4;
+ --tool-panel-search-border-bottom: #EFF1F4;
+
/* container */
--landing-paper-bg: var(--bg-surface);
--landing-inner-paper-bg: #EEF8FF;
@@ -177,7 +197,7 @@
--bg-raised: #1F2329;
--bg-muted: #1F2329;
--bg-background: #2A2F36;
- --bg-toolbar: #272A2E;
+ --bg-toolbar: #1F2329;
--bg-file-manager: #1F2329;
--bg-file-list: #2A2F36;
--btn-open-file: #0A8BFF;
@@ -185,7 +205,7 @@
--text-secondary: #d1d5db;
--text-muted: #9ca3af;
--border-subtle: #2A2F36;
- --border-default: #374151;
+ --border-default: #3A4047;
--border-strong: #4b5563;
--hover-bg: #374151;
--active-bg: #4b5563;
@@ -251,6 +271,27 @@
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4);
+
+ --tools-text-and-icon-color: #D0D6DC;
+
+ /* Tool picker sticky header variables (dark mode) */
+ --tool-header-bg: #2A2F36;
+ --tool-header-border: #3A4047;
+ --tool-header-text: #D0D6DC;
+ --tool-header-badge-bg: #4B525A;
+ --tool-header-badge-text: #FFFFFF;
+
+ /* Subcategory title styling (dark mode) */
+ --tool-subcategory-text-color: #9CA3AF; /* lighter text in dark mode as well */
+ --tool-subcategory-rule-color: #3A4047; /* doubly lighter (relative) line in dark */
+
+ /* Placeholder text colors (dark mode) */
+ --search-text-and-icon-color: #FFFFFF !important;
+
+ /* Tool panel search bar background colors (dark mode) */
+ --tool-panel-search-bg: #1F2329;
+ --tool-panel-search-border-bottom: #4B525A;
+
}
/* Dropzone drop state styling */
diff --git a/frontend/src/types/sidebar.ts b/frontend/src/types/sidebar.ts
index b286f0b82..b93db61d6 100644
--- a/frontend/src/types/sidebar.ts
+++ b/frontend/src/types/sidebar.ts
@@ -32,7 +32,6 @@ export interface ButtonConfig {
id: string;
name: string;
icon: React.ReactNode;
- tooltip: string;
isRound?: boolean;
size?: 'sm' | 'md' | 'lg' | 'xl';
onClick: () => void;
diff --git a/frontend/src/types/tool.ts b/frontend/src/types/tool.ts
index 90e8607da..e5e8c24e2 100644
--- a/frontend/src/types/tool.ts
+++ b/frontend/src/types/tool.ts
@@ -34,7 +34,7 @@ export interface ToolResult {
}
export interface ToolConfiguration {
- maxFiles: number;
+ maxFiles?: number;
supportedFormats?: string[];
}