From 7d9c0b0298c831cb9be93dda31a315f58e0b9071 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 22 Aug 2025 13:53:06 +0100 Subject: [PATCH 1/9] V2 Fix subcategory names in All Tools (and search) pane (#4252) # Description of Changes Because we used string typing for IDs and names, it was really easy to make mistakes where variables named like `subcategory` would be stored as an ID in one file, but then read assuming it's a name in another file. This PR changes the code to consistently use enum cases when referring to IDs of categories, subcategories, and tools (at least in as many places as I can find them, ~I had to add a `ToolId` enum for this work~ I originally added a `ToolId` type for this work, but it caused too many issues when merging with #4222 so I've pulled it back out for now). Making that change made it obvious where we were inconsistently passing IDs and reading them as names etc. allowing me to fix rendering issues in the All Tools pane, where the subcategory IDs were being rendered directly (instead of being translated) or where IDs were being translated into names, but were then being re-translated, causing warnings in the log. --- .../public/locales/en-GB/translation.json | 27 ++- .../src/components/shared/QuickAccessBar.tsx | 10 +- .../src/components/tools/SearchResults.tsx | 6 +- frontend/src/components/tools/ToolPicker.tsx | 26 +- .../tools/shared/SuggestedToolsSection.tsx | 2 +- .../tools/toolPicker/ToolButton.tsx | 6 +- frontend/src/contexts/ToolWorkflowContext.tsx | 4 +- frontend/src/data/toolsTaxonomy.ts | 142 +++++------ .../src/data/useTranslatedToolRegistry.tsx | 223 +++++++++--------- frontend/src/hooks/useSuggestedTools.ts | 20 +- frontend/src/hooks/useToolManagement.tsx | 2 +- frontend/src/hooks/useToolSections.ts | 101 +++++--- 12 files changed, 299 insertions(+), 270 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index e09f874ac..0a03fd01a 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1925,18 +1925,23 @@ "noToolsFound": "No tools found", "allTools": "ALL TOOLS", "quickAccess": "QUICK ACCESS", + "categories": { + "standardTools": "Standard Tools", + "advancedTools": "Advanced Tools", + "recommendedTools": "Recommended Tools" + }, "subcategories": { - "Signing": "Signing", - "Document Security": "Document Security", - "Verification": "Verification", - "Document Review": "Document Review", - "Page Formatting": "Page Formatting", - "Extraction": "Extraction", - "Removal": "Removal", - "Automation": "Automation", - "General": "General", - "Advanced Formatting": "Advanced Formatting", - "Developer Tools": "Developer Tools" + "signing": "Signing", + "documentSecurity": "Document Security", + "verification": "Verification", + "documentReview": "Document Review", + "pageFormatting": "Page Formatting", + "extraction": "Extraction", + "removal": "Removal", + "automation": "Automation", + "general": "General", + "advancedFormatting": "Advanced Formatting", + "developerTools": "Developer Tools" } }, "quickAccess": { diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index bc041a923..80ef86c83 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -13,9 +13,9 @@ import { ButtonConfig } from '../../types/sidebar'; import './quickAccessBar/QuickAccessBar.css'; import AllToolsNavButton from './AllToolsNavButton'; import ActiveToolButton from "./quickAccessBar/ActiveToolButton"; -import { - isNavButtonActive, - getNavButtonStyle, +import { + isNavButtonActive, + getNavButtonStyle, getActiveNavButton, } from './quickAccessBar/QuickAccessBar'; @@ -39,7 +39,7 @@ const QuickAccessBar = forwardRef(({ openFilesModal(); }; - + const buttonConfigs: ButtonConfig[] = [ { id: 'read', @@ -226,4 +226,4 @@ const QuickAccessBar = forwardRef(({ ); }); -export default QuickAccessBar; \ No newline at end of file +export default QuickAccessBar; diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx index 949bd2f64..6ecb64b7e 100644 --- a/frontend/src/components/tools/SearchResults.tsx +++ b/frontend/src/components/tools/SearchResults.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { Box, Stack, Text } from '@mantine/core'; -import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; +import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy'; import ToolButton from './toolPicker/ToolButton'; import { useTranslation } from 'react-i18next'; import { useToolSections } from '../../hooks/useToolSections'; @@ -23,8 +23,8 @@ const SearchResults: React.FC = ({ filteredTools, onSelect } return ( {searchGroups.map(group => ( - - + + {group.tools.map(({ id, tool }) => ( void, showSubcategoryHeader: boolean = true ) => ( - + {showSubcategoryHeader && ( - + )} {subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => ( @@ -69,11 +71,11 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa const { sections: visibleSections } = useToolSections(filteredTools); const quickSection = useMemo( - () => visibleSections.find(s => (s as any).key === 'quick'), + () => visibleSections.find(s => s.key === 'quick'), [visibleSections] ); const allSection = useMemo( - () => visibleSections.find(s => (s as any).key === 'all'), + () => visibleSections.find(s => s.key === 'all'), [visibleSections] ); @@ -120,7 +122,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa {searchGroups.length === 0 ? ( ) : ( - searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect)) + searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect)) )} ) : ( @@ -164,8 +166,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {quickSection?.subcategories.map(sc => - renderToolButtons(sc, selectedToolKey, onSelect, false) + {quickSection?.subcategories.map(sc => + renderToolButtons(t, sc, selectedToolKey, onSelect, false) )} @@ -210,8 +212,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {allSection?.subcategories.map(sc => - renderToolButtons(sc, selectedToolKey, onSelect, true) + {allSection?.subcategories.map(sc => + renderToolButtons(t, sc, selectedToolKey, onSelect, true) )} diff --git a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx index b1f91fc39..da0418571 100644 --- a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx +++ b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx @@ -21,7 +21,7 @@ export function SuggestedToolsSection(): React.ReactElement { const IconComponent = tool.icon; return ( = ({ id, tool, isSelected, onSelect }) => { const handleClick = (id: string) => { if (tool.link) { - // Open external link in new tab + // Open external link in new tab window.open(tool.link, '_blank', 'noopener,noreferrer'); - return; + return; } // Normal tool selection onSelect(id); @@ -47,4 +47,4 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect ); }; -export default ToolButton; \ No newline at end of file +export default ToolButton; diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 4ca66e61c..1ee211312 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -72,7 +72,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { selectedToolKey: string | null; selectedTool: ToolRegistryEntry | null; toolRegistry: any; // From useToolManagement - + // UI Actions setSidebarsVisible: (visible: boolean) => void; setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; @@ -230,4 +230,4 @@ export function useToolWorkflow(): ToolWorkflowContextValue { throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider'); } return context; -} \ No newline at end of file +} diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts index a76752b87..2dff2b8e4 100644 --- a/frontend/src/data/toolsTaxonomy.ts +++ b/frontend/src/data/toolsTaxonomy.ts @@ -3,101 +3,101 @@ import React from 'react'; import { BaseToolProps } from '../types/tool'; 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' + 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 enum ToolCategoryId { + STANDARD_TOOLS = 'standardTools', + ADVANCED_TOOLS = 'advancedTools', + RECOMMENDED_TOOLS = 'recommendedTools' } 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; + 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; + categoryId: ToolCategoryId; + subcategoryId: SubcategoryId; + maxFiles?: number; + supportedFormats?: string[]; + endpoints?: string[]; + link?: string; + type?: string; } -export type ToolRegistry = Record; +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, + 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', + [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 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 getSubcategoryColor = (subcategory: SubcategoryId): string => SUBCATEGORY_COLOR_MAP[subcategory] || '#7882FF'; 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())); + 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); + 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> + registry: ToolRegistry, + extensionToEndpoint?: Record> ): string[] => { - const toolEp = getAllEndpoints(registry); - const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; - return Array.from(new Set([...toolEp, ...convEp])); + 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 index f27fbd5c7..0426a7bf6 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useTranslation } from 'react-i18next'; import SplitPdfPanel from "../tools/Split"; import CompressPdfPanel from "../tools/Compress"; @@ -8,7 +7,7 @@ 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 { SubcategoryId, ToolCategoryId, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; import Repair from '../tools/Repair'; import SingleLargePage from '../tools/SingleLargePage'; @@ -30,8 +29,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.SIGNING }, "sign": { icon: signature, @@ -39,8 +38,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.SIGNING }, @@ -52,8 +51,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"] }, @@ -64,8 +63,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, endpoints: ["add-watermark"] }, "add-stamp": { @@ -74,8 +73,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "sanitize": { icon: cleaning_services, @@ -83,8 +82,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: Sanitize, view: "security", maxFiles: -1, - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"), endpoints: ["sanitize-pdf"] }, @@ -94,8 +93,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "unlock-pdf-forms": { icon: preview_off, @@ -103,8 +102,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: UnlockPdfForms, 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, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["unlock-pdf-forms"] }, @@ -114,8 +113,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "change-permissions": { icon: lock, @@ -123,8 +122,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: ChangePermissions, view: "security", description: t("home.changePermissions.desc", "Change document restrictions and permissions"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"] }, @@ -136,8 +135,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "extract", description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.VERIFICATION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.VERIFICATION }, "validate-pdf-signature": { icon: verified, @@ -145,8 +144,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "security", description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.VERIFICATION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.VERIFICATION }, @@ -158,8 +157,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_REVIEW }, "change-metadata": { icon: assignment, @@ -167,8 +166,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_REVIEW }, // Page Formatting @@ -178,8 +177,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "rotate": { icon: rotate_right, @@ -187,8 +186,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.rotate.desc", "Easily rotate your PDFs."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "splitPdf": { icon: content_cut, @@ -196,8 +195,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: SplitPdfPanel, view: "split", description: t("home.split.desc", "Split PDFs into multiple documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "reorganize-pages": { icon: move_down, @@ -205,8 +204,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "adjust-page-size-scale": { icon: crop_free, @@ -214,8 +213,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "addPageNumbers": { icon: 123, @@ -223,8 +222,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "multi-page-layout": { icon: dashboard, @@ -232,8 +231,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "single-large-page": { icon: looks_one, @@ -241,8 +240,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: SingleLargePage, view: "format", description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["pdf-to-single-page"] }, @@ -252,8 +251,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING, }, @@ -265,8 +264,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "extract", description: t("home.extractPages.desc", "Extract specific pages from a PDF document"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.EXTRACTION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.EXTRACTION }, "extract-images": { icon: filter, @@ -274,8 +273,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "extract", description: t("home.extractImages.desc", "Extract images from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.EXTRACTION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.EXTRACTION }, @@ -287,8 +286,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "remove", description: t("home.removePages.desc", "Remove specific pages from a PDF document"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-blank-pages": { icon: scan_delete, @@ -296,8 +295,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "remove", description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-annotations": { icon: thread_unread, @@ -305,8 +304,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "remove", description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-image": { icon: remove_selection, @@ -314,8 +313,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.removeImagePdf.desc", "Remove images from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-password": { icon: lock_open_right, @@ -323,8 +322,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: RemovePassword, view: "security", description: t("home.removePassword.desc", "Remove password protection from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL, endpoints: ["remove-password"], maxFiles: -1, @@ -335,8 +334,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: RemoveCertificateSign, view: "security", description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-certificate-sign"] }, @@ -350,8 +349,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, "auto-rename-pdf-file": { icon: match_word, @@ -359,8 +358,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, "auto-split-pages": { icon: split_scene_right, @@ -368,8 +367,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.AUTOMATION + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, "auto-split-by-size-count": { icon: content_cut, @@ -377,8 +376,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, @@ -390,8 +389,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "repair": { icon: build, @@ -399,8 +398,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: Repair, view: "format", description: t("home.repair.desc", "Repair corrupted or damaged PDF files"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["repair"] }, @@ -410,8 +409,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "overlay-pdfs": { icon: layers, @@ -419,8 +418,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "replace-and-invert-color": { icon: format_color_fill, @@ -428,8 +427,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "add-image": { icon: image, @@ -437,8 +436,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.addImage.desc", "Add images to PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "edit-table-of-contents": { icon: bookmark_add, @@ -446,8 +445,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "scanner-effect": { icon: scanner, @@ -455,8 +454,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, @@ -468,8 +467,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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 + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS }, "dev-api": { icon: open_in_new, @@ -477,8 +476,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devApi.desc", "Link to API documentation"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html" }, "dev-folder-scanning": { @@ -487,8 +486,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/" }, "dev-sso-guide": { @@ -497,8 +496,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devSsoGuide.desc", "Link to SSO guide"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration", }, "dev-airgapped": { @@ -507,8 +506,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Pro/#activation" }, @@ -520,8 +519,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL }, "compress": { icon: zoom_in_map, @@ -529,8 +528,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: CompressPdfPanel, view: "compress", description: t("home.compress.desc", "Compress PDFs to reduce their file size."), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "convert": { @@ -539,8 +538,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: ConvertPanel, view: "convert", description: t("home.convert.desc", "Convert files to and from PDF format"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: [ "pdf-to-img", @@ -583,8 +582,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "merge", description: t("home.merge.desc", "Merge multiple PDFs into a single document"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "multi-tool": { @@ -593,8 +592,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "pageEditor", description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "ocr": { @@ -603,8 +602,8 @@ export function useFlatToolRegistry(): ToolRegistry { 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, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "redact": { @@ -613,8 +612,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "redact", description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL }, }; diff --git a/frontend/src/hooks/useSuggestedTools.ts b/frontend/src/hooks/useSuggestedTools.ts index ae6d77035..effbe2397 100644 --- a/frontend/src/hooks/useSuggestedTools.ts +++ b/frontend/src/hooks/useSuggestedTools.ts @@ -9,7 +9,7 @@ import CropIcon from '@mui/icons-material/Crop'; import TextFieldsIcon from '@mui/icons-material/TextFields'; export interface SuggestedTool { - name: string; + id: string /* FIX ME: Should be ToolId */; title: string; icon: React.ComponentType; navigate: () => void; @@ -17,27 +17,27 @@ export interface SuggestedTool { const ALL_SUGGESTED_TOOLS: Omit[] = [ { - name: 'compress', + id: 'compress', title: 'Compress', icon: CompressIcon }, { - name: 'convert', + id: 'convert', title: 'Convert', icon: SwapHorizIcon }, { - name: 'sanitize', + id: 'sanitize', title: 'Sanitize', icon: CleaningServicesIcon }, { - name: 'split', + id: 'split', title: 'Split', icon: CropIcon }, { - name: 'ocr', + id: 'ocr', title: 'OCR', icon: TextFieldsIcon } @@ -48,12 +48,12 @@ export function useSuggestedTools(): SuggestedTool[] { return useMemo(() => { // 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 return filteredTools.map(tool => ({ ...tool, - navigate: () => handleToolSelect(tool.name) + navigate: () => handleToolSelect(tool.id) })); }, [selectedToolKey, handleToolSelect]); -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index e991677a9..a4c481fcc 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -17,7 +17,7 @@ interface ToolManagementResult { export const useToolManagement = (): ToolManagementResult => { const { t } = useTranslation(); - const [selectedToolKey, setSelectedToolKey] = useState(null); + const [selectedToolKey, setSelectedToolKey] = useState(null); const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]); // Build endpoints list from registry entries with fallback to legacy mapping diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts index 4c1a0c05d..41762a8e1 100644 --- a/frontend/src/hooks/useToolSections.ts +++ b/frontend/src/hooks/useToolSections.ts @@ -1,65 +1,87 @@ import { useMemo } from 'react'; -import { SUBCATEGORY_ORDER, ToolCategory, ToolRegistryEntry } from '../data/toolsTaxonomy'; +import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy'; import { useTranslation } from 'react-i18next'; +type SubcategoryIdMap = { + [subcategoryId in SubcategoryId]: Array<{ id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }>; +} + type GroupedTools = { - [category: string]: { - [subcategory: string]: Array<{ id: string; tool: ToolRegistryEntry }>; - }; + [categoryId in ToolCategoryId]: SubcategoryIdMap; }; -export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) { +export interface SubcategoryGroup { + subcategoryId: SubcategoryId; + tools: { + id: string /* FIX ME: Should be ToolId */; + tool: ToolRegistryEntry; + }[]; +}; + +export type ToolSectionKey = 'quick' | 'all'; + +export interface ToolSection { + key: ToolSectionKey; + title: string; + subcategories: SubcategoryGroup[]; +}; + +export function useToolSections(filteredTools: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry][]) { const { t } = useTranslation(); const groupedTools = useMemo(() => { - const grouped: GroupedTools = {}; + const grouped = {} as 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 }); + const categoryId = tool.categoryId; + const subcategoryId = tool.subcategoryId; + if (!grouped[categoryId]) grouped[categoryId] = {} as SubcategoryIdMap; + if (!grouped[categoryId][subcategoryId]) grouped[categoryId][subcategoryId] = []; + grouped[categoryId][subcategoryId].push({ id, tool }); }); return grouped; }, [filteredTools]); - const sections = useMemo(() => { - const getOrderIndex = (name: string) => { - const idx = SUBCATEGORY_ORDER.indexOf(name as any); + const sections: ToolSection[] = useMemo(() => { + const getOrderIndex = (id: SubcategoryId) => { + const idx = SUBCATEGORY_ORDER.indexOf(id); return idx === -1 ? Number.MAX_SAFE_INTEGER : idx; }; - const quick: Record> = {}; - const all: Record> = {}; + const quick = {} as SubcategoryIdMap; + const all = {} as SubcategoryIdMap; - Object.entries(groupedTools).forEach(([origCat, subs]) => { - const upperCat = origCat.toUpperCase(); + Object.entries(groupedTools).forEach(([c, subs]) => { + const categoryId = c as ToolCategoryId; - Object.entries(subs).forEach(([sub, tools]) => { - if (!all[sub]) all[sub] = []; - all[sub].push(...tools); + Object.entries(subs).forEach(([s, tools]) => { + const subcategoryId = s as SubcategoryId; + if (!all[subcategoryId]) all[subcategoryId] = []; + all[subcategoryId].push(...tools); }); - if (upperCat === ToolCategory.RECOMMENDED_TOOLS.toUpperCase()) { - Object.entries(subs).forEach(([sub, tools]) => { - if (!quick[sub]) quick[sub] = []; - quick[sub].push(...tools); + if (categoryId === ToolCategoryId.RECOMMENDED_TOOLS) { + Object.entries(subs).forEach(([s, tools]) => { + const subcategoryId = s as SubcategoryId; + if (!quick[subcategoryId]) quick[subcategoryId] = []; + quick[subcategoryId].push(...tools); }); } }); - const sortSubs = (obj: Record>) => + const sortSubs = (obj: SubcategoryIdMap) => Object.entries(obj) .sort(([a], [b]) => { - const ai = getOrderIndex(a); - const bi = getOrderIndex(b); + const aId = a as SubcategoryId; + const bId = b as SubcategoryId; + const ai = getOrderIndex(aId); + const bi = getOrderIndex(bId); 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: '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)); }, [groupedTools]); - const searchGroups = useMemo(() => { - const subMap: Record> = {}; - const seen = new Set(); + const searchGroups: SubcategoryGroup[] = useMemo(() => { + const subMap = {} as SubcategoryIdMap; + const seen = new Set(); filteredTools.forEach(([id, tool]) => { - if (seen.has(id)) return; - seen.add(id); - const sub = tool.subcategory; + const toolId = id as string /* FIX ME: Should be ToolId */; + if (seen.has(toolId)) return; + seen.add(toolId); + const sub = tool.subcategoryId; if (!subMap[sub]) subMap[sub] = []; - subMap[sub].push({ id, tool }); + subMap[sub].push({ id: toolId, tool }); }); return Object.entries(subMap) .sort(([a], [b]) => a.localeCompare(b)) - .map(([subcategory, tools]) => ({ subcategory, tools })); + .map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup)); }, [filteredTools]); return { sections, searchGroups }; From 23d86deae71e0e5cbf8c0af1bab5e5d54e30a7f8 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:40:27 +0100 Subject: [PATCH 2/9] Feature/v2/automate (#4248) * automate feature * Moved all providers to app level to simplify homepage * Circular dependency fixes * You will see that now toolRegistry gets a tool config and a tool settings object. These enable automate to run the tools using as much static code as possible. --------- Co-authored-by: Connor Yoh --- frontend/package-lock.json | 1431 ++++++++++++++++- frontend/package.json | 1 + .../public/locales/en-GB/translation.json | 80 +- frontend/src/App.tsx | 57 +- frontend/src/components/FileManager.tsx | 4 +- .../src/components/shared/ErrorBoundary.tsx | 56 + frontend/src/components/tools/ToolPicker.tsx | 36 +- .../AddWatermarkSingleStepSettings.tsx | 70 + .../addWatermark/WatermarkFormatting.tsx | 2 +- .../tools/addWatermark/WatermarkImageFile.tsx | 4 +- .../addWatermark/WatermarkStyleSettings.tsx | 10 +- .../tools/addWatermark/WatermarkTextStyle.tsx | 2 +- .../tools/addWatermark/WatermarkWording.tsx | 2 +- .../tools/automate/AutomationCreation.tsx | 199 +++ .../tools/automate/AutomationEntry.tsx | 163 ++ .../tools/automate/AutomationRun.tsx | 223 +++ .../tools/automate/AutomationSelection.tsx | 76 + .../tools/automate/ToolConfigurationModal.tsx | 138 ++ .../components/tools/automate/ToolList.tsx | 149 ++ .../tools/automate/ToolSelector.tsx | 182 +++ .../tools/shared/SuggestedToolsSection.tsx | 1 + .../tools/shared/createToolFlow.tsx | 3 +- .../tools/shared/renderToolButtons.tsx | 34 + .../tools/toolPicker/ToolSearch.tsx | 50 +- frontend/src/constants/automation.ts | 42 + frontend/src/contexts/NavigationContext.tsx | 77 +- frontend/src/contexts/ToolWorkflowContext.tsx | 117 +- frontend/src/data/toolsTaxonomy.ts | 30 +- .../src/data/useTranslatedToolRegistry.tsx | 106 +- .../useAddPasswordOperation.test.ts | 2 +- .../addPassword/useAddPasswordOperation.ts | 49 +- .../addWatermark/useAddWatermarkOperation.ts | 20 +- .../addWatermark/useAddWatermarkParameters.ts | 1 + .../tools/automate/useAutomateOperation.ts | 49 + .../hooks/tools/automate/useAutomationForm.ts | 114 ++ .../tools/automate/useSavedAutomations.ts | 55 + .../tools/automate/useSuggestedAutomations.ts | 53 + .../useChangePermissionsOperation.test.ts | 4 +- .../useChangePermissionsOperation.ts | 43 +- .../tools/compress/useCompressOperation.ts | 23 +- .../tools/compress/useCompressParameters.ts | 2 +- .../tools/convert/useConvertOperation.ts | 112 +- .../tools/convert/useConvertParameters.ts | 29 +- .../src/hooks/tools/ocr/useOCROperation.ts | 104 +- .../src/hooks/tools/ocr/useOCRParameters.ts | 2 +- .../useRemoveCertificateSignOperation.ts | 30 +- .../useRemovePasswordOperation.test.ts | 2 +- .../useRemovePasswordOperation.ts | 32 +- .../hooks/tools/repair/useRepairOperation.ts | 30 +- .../tools/sanitize/useSanitizeOperation.ts | 20 +- .../hooks/tools/shared/useToolOperation.ts | 3 + .../useSingleLargePageOperation.ts | 30 +- .../hooks/tools/split/useSplitOperation.ts | 24 +- .../hooks/tools/split/useSplitParameters.ts | 2 +- .../useUnlockPdfFormsOperation.ts | 30 +- frontend/src/hooks/useSuggestedTools.ts | 9 +- frontend/src/hooks/useToolManagement.tsx | 35 +- frontend/src/hooks/useUrlSync.ts | 2 +- frontend/src/pages/HomePage.tsx | 31 +- frontend/src/services/automationStorage.ts | 183 +++ .../tests/convert/ConvertIntegration.test.tsx | 2 +- frontend/src/tools/AddPassword.tsx | 6 +- frontend/src/tools/AddWatermark.tsx | 7 +- frontend/src/tools/Automate.tsx | 168 ++ frontend/src/tools/ChangePermissions.tsx | 7 +- frontend/src/tools/Compress.tsx | 7 +- frontend/src/tools/Convert.tsx | 7 +- frontend/src/tools/OCR.tsx | 7 +- frontend/src/tools/RemoveCertificateSign.tsx | 7 +- frontend/src/tools/RemovePassword.tsx | 7 +- frontend/src/tools/Repair.tsx | 7 +- frontend/src/tools/Sanitize.tsx | 9 +- frontend/src/tools/SingleLargePage.tsx | 7 +- frontend/src/tools/Split.tsx | 4 +- frontend/src/tools/UnlockPdfForms.tsx | 7 +- frontend/src/types/automation.ts | 70 + frontend/src/types/navigation.ts | 42 + frontend/src/types/navigationActions.ts | 21 + frontend/src/types/tool.ts | 24 + frontend/src/utils/automationExecutor.ts | 157 ++ frontend/src/utils/automationFileProcessor.ts | 186 +++ frontend/src/utils/convertUtils.ts | 33 +- frontend/src/utils/resourceManager.ts | 71 + frontend/src/utils/urlRouting.ts | 23 +- 84 files changed, 4784 insertions(+), 572 deletions(-) create mode 100644 frontend/src/components/shared/ErrorBoundary.tsx create mode 100644 frontend/src/components/tools/addWatermark/AddWatermarkSingleStepSettings.tsx create mode 100644 frontend/src/components/tools/automate/AutomationCreation.tsx create mode 100644 frontend/src/components/tools/automate/AutomationEntry.tsx create mode 100644 frontend/src/components/tools/automate/AutomationRun.tsx create mode 100644 frontend/src/components/tools/automate/AutomationSelection.tsx create mode 100644 frontend/src/components/tools/automate/ToolConfigurationModal.tsx create mode 100644 frontend/src/components/tools/automate/ToolList.tsx create mode 100644 frontend/src/components/tools/automate/ToolSelector.tsx create mode 100644 frontend/src/components/tools/shared/renderToolButtons.tsx create mode 100644 frontend/src/constants/automation.ts create mode 100644 frontend/src/hooks/tools/automate/useAutomateOperation.ts create mode 100644 frontend/src/hooks/tools/automate/useAutomationForm.ts create mode 100644 frontend/src/hooks/tools/automate/useSavedAutomations.ts create mode 100644 frontend/src/hooks/tools/automate/useSuggestedAutomations.ts create mode 100644 frontend/src/services/automationStorage.ts create mode 100644 frontend/src/tools/Automate.tsx create mode 100644 frontend/src/types/automation.ts create mode 100644 frontend/src/types/navigation.ts create mode 100644 frontend/src/types/navigationActions.ts create mode 100644 frontend/src/utils/automationExecutor.ts create mode 100644 frontend/src/utils/automationFileProcessor.ts create mode 100644 frontend/src/utils/resourceManager.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f9ec204a6..1438432a5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -48,6 +48,7 @@ "@vitest/coverage-v8": "^1.0.0", "jsdom": "^23.0.0", "license-checker": "^25.0.1", + "madge": "^8.0.0", "postcss": "^8.5.3", "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.17.0", @@ -331,12 +332,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", - "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -419,9 +420,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -553,6 +554,20 @@ "node": ">=18" } }, + "node_modules/@dependents/detective-less": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.1.tgz", + "integrity": "sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2367,6 +2382,96 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@ts-graphviz/adapter": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ts-graphviz/adapter/-/adapter-2.0.6.tgz", + "integrity": "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/ast": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/ast/-/ast-2.0.7.tgz", + "integrity": "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/common": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ts-graphviz/common/-/common-2.1.5.tgz", + "integrity": "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/core": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/core/-/core-2.0.7.tgz", + "integrity": "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -2475,6 +2580,132 @@ "@types/react": "*" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", @@ -2668,6 +2899,94 @@ "dev": true, "license": "MIT" }, + "node_modules/@vue/compiler-core": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.19.tgz", + "integrity": "sha512-/afpyvlkrSNYbPo94Qu8GtIOWS+g5TRdOvs6XZNw6pWQQmj5pBgSZvEPOIZlqWq0YvoUhDDQaQ2TnzuJdOV4hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.19", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.19.tgz", + "integrity": "sha512-Drs6rPHQZx/pN9S6ml3Z3K/TWCIRPvzG2B/o5kFK9X0MNHt8/E+38tiRfojufrYBfA6FQUFB2qBBRXlcSXWtOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.19", + "@vue/shared": "3.5.19" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.19.tgz", + "integrity": "sha512-YWCm1CYaJ+2RvNmhCwI7t3I3nU+hOrWGWMsn+Z/kmm1jy5iinnVtlmkiZwbLlbV1SRizX7vHsc0/bG5dj0zRTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.19", + "@vue/compiler-dom": "3.5.19", + "@vue/compiler-ssr": "3.5.19", + "@vue/shared": "3.5.19", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.19.tgz", + "integrity": "sha512-/wx0VZtkWOPdiQLWPeQeqpHWR/LuNC7bHfSX7OayBTtUy8wur6vT6EQIX6Et86aED6J+y8tTw43qo2uoqGg5sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.19", + "@vue/shared": "3.5.19" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.19.tgz", + "integrity": "sha512-IhXCOn08wgKrLQxRFKKlSacWg4Goi1BolrdEeLYn6tgHjJNXVrWJ5nzoxZqNwl5p88aLlQ8LOaoMa3AYvaKJ/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2738,6 +3057,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2752,6 +3078,13 @@ "node": ">= 8" } }, + "node_modules/app-module-path": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", + "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -2808,6 +3141,16 @@ "node": "*" } }, + "node_modules/ast-module-types": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-6.0.1.tgz", + "integrity": "sha512-WHw67kLXYbZuHTmcdbIrVArCq5wxo6NEuj3hiYAWr8mwJeC+C2mMCIBIWCiDoCye/OF/xelc+teJ1ERoWmnEIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2893,6 +3236,27 @@ "devOptional": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -2922,6 +3286,18 @@ "integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==", "license": "MIT" }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2978,6 +3354,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3152,6 +3553,42 @@ "node": ">=10" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3201,6 +3638,23 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3424,6 +3878,29 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3450,6 +3927,35 @@ "node": ">=4" } }, + "node_modules/dependency-tree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.2.0.tgz", + "integrity": "sha512-+C1H3mXhcvMCeu5i2Jpg9dc0N29TWTuT6vJD7mHLAfVmAbo9zW8NlkvQ1tYd3PDMab0IRQM0ccoyX68EZtx9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "filing-cabinet": "^5.0.3", + "precinct": "^12.2.0", + "typescript": "^5.8.3" + }, + "bin": { + "dependency-tree": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-tree/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3474,6 +3980,147 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/detective-amd": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-6.0.1.tgz", + "integrity": "sha512-TtyZ3OhwUoEEIhTFoc1C9IyJIud3y+xYkSRjmvCt65+ycQuc3VcBrPRTMWoO/AnuCyOB8T5gky+xf7Igxtjd3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "escodegen": "^2.1.0", + "get-amd-module-type": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "bin": { + "detective-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-cjs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.0.1.tgz", + "integrity": "sha512-tLTQsWvd2WMcmn/60T2inEJNhJoi7a//PQ7DwRKEj1yEeiQs4mrONgsUtEJKnZmrGWBBmE0kJ1vqOG/NAxwaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-es6": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-5.0.1.tgz", + "integrity": "sha512-XusTPuewnSUdoxRSx8OOI6xIA/uld/wMQwYsouvFN2LAg7HgP06NF1lHRV3x6BZxyL2Kkoih4ewcq8hcbGtwew==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-postcss": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detective-postcss/-/detective-postcss-7.0.1.tgz", + "integrity": "sha512-bEOVpHU9picRZux5XnwGsmCN4+8oZo7vSW0O0/Enq/TO5R2pIAP2279NsszpJR7ocnQt4WXU0+nnh/0JuK4KHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-url": "^1.2.4", + "postcss-values-parser": "^6.0.2" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.47" + } + }, + "node_modules/detective-sass": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-sass/-/detective-sass-6.0.1.tgz", + "integrity": "sha512-jSGPO8QDy7K7pztUmGC6aiHkexBQT4GIH+mBAL9ZyBmnUIOFbkfZnO8wPRRJFP/QP83irObgsZHCoDHZ173tRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-scss": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-scss/-/detective-scss-5.0.1.tgz", + "integrity": "sha512-MAyPYRgS6DCiS6n6AoSBJXLGVOydsr9huwXORUlJ37K3YLyiN0vYHpzs3AdJOgHobBfispokoqrEon9rbmKacg==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-stylus": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-stylus/-/detective-stylus-5.0.1.tgz", + "integrity": "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-typescript": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-14.0.0.tgz", + "integrity": "sha512-pgN43/80MmWVSEi5LUuiVvO/0a9ss5V7fwVfrJ4QzAQRd3cwqU1SfWGXJFcNKUqoD5cS+uIovhw5t/0rSeC5Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "^8.23.0", + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4" + } + }, + "node_modules/detective-vue2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/detective-vue2/-/detective-vue2-2.2.0.tgz", + "integrity": "sha512-sVg/t6O2z1zna8a/UIV6xL5KUa2cMTQbdTIIvqNM0NIPswp52fe43Nwmbahzj3ww4D844u/vC2PYfiGLvD3zFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^5.0.1", + "@vue/compiler-sfc": "^3.5.13", + "detective-es6": "^5.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -3672,6 +4319,76 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -3682,6 +4399,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -3771,6 +4498,42 @@ "node": ">= 12" } }, + "node_modules/filing-cabinet": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.0.3.tgz", + "integrity": "sha512-PlPcMwVWg60NQkhvfoxZs4wEHjhlOO/y7OAm4sKM60o1Z9nttRY4mcdQxp/iZ+kg/Vv6Hw1OAaTbYVM9DA9pYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-module-path": "^2.2.0", + "commander": "^12.1.0", + "enhanced-resolve": "^5.18.0", + "module-definition": "^6.0.1", + "module-lookup-amd": "^9.0.3", + "resolve": "^1.22.10", + "resolve-dependency-path": "^4.0.1", + "sass-lookup": "^6.1.0", + "stylus-lookup": "^6.1.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3" + }, + "bin": { + "filing-cabinet": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/filing-cabinet/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3934,6 +4697,20 @@ "node": ">=6.9.0" } }, + "node_modules/get-amd-module-type": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-6.0.1.tgz", + "integrity": "sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3987,6 +4764,13 @@ "node": ">=6" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4044,6 +4828,22 @@ "node": ">=4" } }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4273,6 +5073,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -4321,6 +5142,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4388,6 +5216,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4398,6 +5236,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4405,6 +5253,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -4418,6 +5276,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-url-superb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", + "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -5062,6 +5953,23 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5103,6 +6011,45 @@ "lz-string": "bin/bin.js" } }, + "node_modules/madge": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/madge/-/madge-8.0.0.tgz", + "integrity": "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^7.2.0", + "commondir": "^1.0.1", + "debug": "^4.3.4", + "dependency-tree": "^11.0.0", + "ora": "^5.4.1", + "pluralize": "^8.0.0", + "pretty-ms": "^7.0.1", + "rc": "^1.2.8", + "stream-to-array": "^2.3.0", + "ts-graphviz": "^2.1.2", + "walkdir": "^0.4.1" + }, + "bin": { + "madge": "bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.me/pahen" + }, + "peerDependencies": { + "typescript": "^5.4.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -5359,6 +6306,52 @@ "dev": true, "license": "MIT" }, + "node_modules/module-definition": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.1.tgz", + "integrity": "sha512-FeVc50FTfVVQnolk/WQT8MX+2WVcDnTGiq6Wo+/+lJ2ET1bRVi3HG3YlJUfqagNMc/kUlFSoR96AJkxGpKz13g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "bin": { + "module-definition": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-lookup-amd": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-9.0.5.tgz", + "integrity": "sha512-Rs5FVpVcBYRHPLuhHOjgbRhosaQYLtEo3JIeDIbmNo7mSssi1CTzwMh8v36gAzpbzLGXI9wB/yHh+5+3fY1QVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "glob": "^7.2.3", + "requirejs": "^2.3.7", + "requirejs-config-file": "^4.0.0" + }, + "bin": { + "lookup-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-lookup-amd/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5438,6 +6431,19 @@ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "license": "MIT" }, + "node_modules/node-source-walk": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.1.tgz", + "integrity": "sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.7" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -5578,6 +6584,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -5658,6 +6688,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -5857,10 +6897,20 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5877,7 +6927,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6186,6 +7236,64 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postcss-values-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "color-name": "^1.1.4", + "is-url-superb": "^4.0.0", + "quote-unquote": "^1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.2.9" + } + }, + "node_modules/precinct": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", + "integrity": "sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^5.0.1", + "commander": "^12.1.0", + "detective-amd": "^6.0.1", + "detective-cjs": "^6.0.1", + "detective-es6": "^5.0.1", + "detective-postcss": "^7.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.0.0", + "detective-vue2": "^2.2.0", + "module-definition": "^6.0.1", + "node-source-walk": "^7.0.1", + "postcss": "^8.5.1", + "typescript": "^5.7.3" + }, + "bin": { + "precinct": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/precinct/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -6228,6 +7336,22 @@ "node": ">= 0.8" } }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6307,12 +7431,35 @@ ], "license": "MIT" }, + "node_modules/quote-unquote": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", + "dev": true, + "license": "MIT" + }, "node_modules/raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -6596,8 +7743,8 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6666,6 +7813,34 @@ "node": ">=0.10.0" } }, + "node_modules/requirejs": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz", + "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==", + "dev": true, + "license": "MIT", + "bin": { + "r_js": "bin/r.js", + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/requirejs-config-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz", + "integrity": "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esprima": "^4.0.0", + "stringify-object": "^3.2.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -6693,6 +7868,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dependency-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/resolve-dependency-path/-/resolve-dependency-path-4.0.1.tgz", + "integrity": "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6702,6 +7887,46 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6805,6 +8030,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -6819,8 +8045,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -6829,6 +8054,33 @@ "dev": true, "license": "MIT" }, + "node_modules/sass-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.0.tgz", + "integrity": "sha512-Zx+lVyoWqXZxHuYWlTA17Z5sczJ6braNT2C7rmClw+c4E7r/n911Zwss3h1uHI9reR5AgHZyNHF7c2+VIp5AUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "enhanced-resolve": "^5.18.0" + }, + "bin": { + "sass-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sass-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -6913,8 +8165,8 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -7050,12 +8302,22 @@ "dev": true, "license": "MIT" }, + "node_modules/stream-to-array": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", + "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -7082,6 +8344,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7095,6 +8372,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -7120,6 +8407,16 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-literal": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", @@ -7146,6 +8443,32 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, + "node_modules/stylus-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/stylus-lookup/-/stylus-lookup-6.1.0.tgz", + "integrity": "sha512-5QSwgxAzXPMN+yugy61C60PhoANdItfdjSEZR8siFwz7yL9jTmV0UBKDCfn3K8GkGB4g0Y9py7vTCX8rFu4/pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0" + }, + "bin": { + "stylus-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylus-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/sugarss": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", @@ -7418,6 +8741,60 @@ "node": ">=0.6" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-graphviz": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-2.1.6.tgz", + "integrity": "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/adapter": "^2.0.6", + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5", + "@ts-graphviz/core": "^2.0.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8827,6 +10204,26 @@ "node": ">=18" } }, + "node_modules/walkdir": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-vitals": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index ad945dbc2..cde323bcc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -74,6 +74,7 @@ "@vitest/coverage-v8": "^1.0.0", "jsdom": "^23.0.0", "license-checker": "^25.0.1", + "madge": "^8.0.0", "postcss": "^8.5.3", "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.17.0", diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 0a03fd01a..63937bc74 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -85,6 +85,7 @@ "warning": { "tooltipTitle": "Warning" }, + "edit": "Edit", "delete": "Delete", "username": "Username", "password": "Password", @@ -538,10 +539,6 @@ "title": "Edit Table of Contents", "desc": "Add or edit bookmarks and table of contents in PDF documents" }, - "automate": { - "title": "Automate", - "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." - }, "manageCertificates": { "title": "Manage Certificates", "desc": "Import, export, or delete digital certificate files used for signing PDFs." @@ -601,6 +598,10 @@ "changePermissions": { "title": "Change Permissions", "desc": "Change document restrictions and permissions" + }, + "automate": { + "title": "Automate", + "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." } }, "viewPdf": { @@ -731,7 +732,8 @@ "officeDocs": "Office Documents (Word, Excel, PowerPoint)", "imagesExt": "Images (JPG, PNG, etc.)", "markdown": "Markdown", - "textRtf": "Text/RTF" + "textRtf": "Text/RTF", + "grayscale": "Greyscale" }, "imageToPdf": { "tags": "conversion,img,jpg,picture,photo" @@ -2021,7 +2023,8 @@ "downloadSelected": "Download Selected", "selectedCount": "{{count}} selected", "download": "Download", - "delete": "Delete" + "delete": "Delete", + "unsupported":"Unsupported" }, "storage": { "temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically", @@ -2191,5 +2194,68 @@ "results": { "title": "Decrypted PDFs" } - } + }, + "automate": { + "title": "Automate", + "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks.", + "invalidStep": "Invalid step", + "files": { + "placeholder": "Select files to process with this automation" + }, + "selection": { + "title": "Automation Selection", + "saved": { + "title": "Saved" + }, + "createNew": { + "title": "Create New Automation" + }, + "suggested": { + "title": "Suggested" + } + }, + "creation": { + "createTitle": "Create Automation", + "editTitle": "Edit Automation", + "description": "Automations run tools sequentially. To get started, add tools in the order you want them to run.", + "name": { + "placeholder": "Automation name" + }, + "tools": { + "selectTool": "Select a tool...", + "selected": "Selected Tools", + "remove": "Remove tool", + "configure": "Configure tool", + "notConfigured": "! Not Configured", + "addTool": "Add Tool", + "add": "Add a tool..." + }, + "save": "Save Automation", + "unsavedChanges": { + "title": "Unsaved Changes", + "message": "You have unsaved changes. Are you sure you want to go back? All changes will be lost.", + "cancel": "Cancel", + "confirm": "Go Back" + } + }, + "run": { + "title": "Run Automation" + }, + "sequence": { + "unnamed": "Unnamed Automation", + "steps": "{{count}} steps", + "running": "Running Automation...", + "run": "Run Automation", + "finish": "Finish" + }, + "reviewTitle": "Automation Results", + "config": { + "loading": "Loading tool configuration...", + "noSettings": "This tool does not have configurable settings.", + "title": "Configure {{toolName}}", + "description": "Configure the settings for this tool. These settings will be applied when the automation runs.", + "cancel": "Cancel", + "save": "Save Configuration" + } + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e628dc4de..b498b0677 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,24 +1,29 @@ -import React, { Suspense } from 'react'; -import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider'; -import { FileContextProvider } from './contexts/FileContext'; -import { NavigationProvider } from './contexts/NavigationContext'; -import { FilesModalProvider } from './contexts/FilesModalContext'; -import HomePage from './pages/HomePage'; +import React, { Suspense } from "react"; +import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider"; +import { FileContextProvider } from "./contexts/FileContext"; +import { NavigationProvider } from "./contexts/NavigationContext"; +import { FilesModalProvider } from "./contexts/FilesModalContext"; +import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext"; +import { SidebarProvider } from "./contexts/SidebarContext"; +import ErrorBoundary from "./components/shared/ErrorBoundary"; +import HomePage from "./pages/HomePage"; // Import global styles -import './styles/tailwind.css'; -import './index.css'; +import "./styles/tailwind.css"; +import "./index.css"; // Loading component for i18next suspense const LoadingFallback = () => ( -
+
Loading...
); @@ -27,13 +32,19 @@ export default function App() { return ( }> - - - - - - - + + + + + + + + + + + + + ); diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx index 1c327cefa..4294180f3 100644 --- a/frontend/src/components/FileManager.tsx +++ b/frontend/src/components/FileManager.tsx @@ -111,7 +111,7 @@ const FileManager: React.FC = ({ selectedTool }) => { onClose={closeFilesModal} size={isMobile ? "100%" : "auto"} centered - radius={30} + radius="md" className="overflow-hidden p-0" withCloseButton={false} styles={{ @@ -144,7 +144,7 @@ const FileManager: React.FC = ({ selectedTool }) => { height: '100%', width: '100%', border: 'none', - borderRadius: '30px', + borderRadius: 'var(--radius-md)', backgroundColor: 'var(--bg-file-manager)' }} styles={{ diff --git a/frontend/src/components/shared/ErrorBoundary.tsx b/frontend/src/components/shared/ErrorBoundary.tsx new file mode 100644 index 000000000..3dfd32d27 --- /dev/null +++ b/frontend/src/components/shared/ErrorBoundary.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Text, Button, Stack } from '@mantine/core'; + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; + fallback?: React.ComponentType<{error?: Error; retry: () => void}>; +} + +export default class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + retry = () => { + this.setState({ hasError: false, error: undefined }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + const Fallback = this.props.fallback; + return ; + } + + return ( + + Something went wrong + {process.env.NODE_ENV === 'development' && this.state.error && ( + + {this.state.error.message} + + )} + + + ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index 411ecdb7a..a8dbd7993 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -1,13 +1,11 @@ import React, { useMemo, useRef, useLayoutEffect, useState } from "react"; -import { Box, Text, Stack } from "@mantine/core"; +import { Box, Stack } from "@mantine/core"; import { useTranslation } from "react-i18next"; -import { getSubcategoryLabel, ToolRegistryEntry } from "../../data/toolsTaxonomy"; -import ToolButton from "./toolPicker/ToolButton"; +import { ToolRegistryEntry } from "../../data/toolsTaxonomy"; import "./toolPicker/ToolPicker.css"; -import { SubcategoryGroup, useToolSections } from "../../hooks/useToolSections"; -import SubcategoryHeader from "./shared/SubcategoryHeader"; +import { useToolSections } from "../../hooks/useToolSections"; import NoToolsFound from "./shared/NoToolsFound"; -import { TFunction } from "i18next"; +import { renderToolButtons } from "./shared/renderToolButtons"; interface ToolPickerProps { selectedToolKey: string | null; @@ -16,32 +14,6 @@ interface ToolPickerProps { isSearching?: boolean; } -// Helper function to render tool buttons for a subcategory -const renderToolButtons = ( - t: TFunction, - subcategory: SubcategoryGroup, - 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); diff --git a/frontend/src/components/tools/addWatermark/AddWatermarkSingleStepSettings.tsx b/frontend/src/components/tools/addWatermark/AddWatermarkSingleStepSettings.tsx new file mode 100644 index 000000000..59ed48e95 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/AddWatermarkSingleStepSettings.tsx @@ -0,0 +1,70 @@ +/** + * AddWatermarkSingleStepSettings - Used for automation only + * + * This component combines all watermark settings into a single step interface + * for use in the automation system. It includes type selection and all relevant + * settings in one unified component. + */ + +import React from "react"; +import { Stack } from "@mantine/core"; +import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; +import WatermarkTypeSettings from "./WatermarkTypeSettings"; +import WatermarkWording from "./WatermarkWording"; +import WatermarkTextStyle from "./WatermarkTextStyle"; +import WatermarkImageFile from "./WatermarkImageFile"; +import WatermarkFormatting from "./WatermarkFormatting"; + +interface AddWatermarkSingleStepSettingsProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; + disabled?: boolean; +} + +const AddWatermarkSingleStepSettings = ({ parameters, onParameterChange, disabled = false }: AddWatermarkSingleStepSettingsProps) => { + return ( + + {/* Watermark Type Selection */} + onParameterChange("watermarkType", type)} + disabled={disabled} + /> + + {/* Conditional settings based on watermark type */} + {parameters.watermarkType === "text" && ( + <> + + + + )} + + {parameters.watermarkType === "image" && ( + + )} + + {/* Formatting settings for both text and image */} + {parameters.watermarkType && ( + + )} + + ); +}; + +export default AddWatermarkSingleStepSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx b/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx index 9a267f638..b6af3365c 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx @@ -6,7 +6,7 @@ import NumberInputWithUnit from "../shared/NumberInputWithUnit"; interface WatermarkFormattingProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx b/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx index 6f38ae206..85e723ccb 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx @@ -6,7 +6,7 @@ import FileUploadButton from "../../shared/FileUploadButton"; interface WatermarkImageFileProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } @@ -17,7 +17,7 @@ const WatermarkImageFile = ({ parameters, onParameterChange, disabled = false }: onParameterChange('watermarkImage', file)} + onChange={(file) => onParameterChange('watermarkImage', file || undefined)} accept="image/*" disabled={disabled} placeholder={t('watermark.settings.image.choose', 'Choose Image')} diff --git a/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx index 2de9335b0..f3c6751cf 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx @@ -5,7 +5,7 @@ import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAdd interface WatermarkStyleSettingsProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } @@ -19,7 +19,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.rotation', 'Rotation (degrees)')} onParameterChange('rotation', value || 0)} + onChange={(value) => onParameterChange('rotation', typeof value === 'number' ? value : (parseInt(value as string, 10) || 0))} min={-360} max={360} disabled={disabled} @@ -28,7 +28,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.opacity', 'Opacity (%)')} onParameterChange('opacity', value || 50)} + onChange={(value) => onParameterChange('opacity', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))} min={0} max={100} disabled={disabled} @@ -40,7 +40,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.spacing.width', 'Width Spacing')} onParameterChange('widthSpacer', value || 50)} + onChange={(value) => onParameterChange('widthSpacer', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))} min={0} max={200} disabled={disabled} @@ -49,7 +49,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.spacing.height', 'Height Spacing')} onParameterChange('heightSpacer', value || 50)} + onChange={(value) => onParameterChange('heightSpacer', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))} min={0} max={200} disabled={disabled} diff --git a/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx b/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx index 00fd21d09..91217f76b 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx @@ -6,7 +6,7 @@ import { alphabetOptions } from "../../../constants/addWatermarkConstants"; interface WatermarkTextStyleProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/addWatermark/WatermarkWording.tsx b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx index 621a0f399..5278ca332 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkWording.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx @@ -6,7 +6,7 @@ import { removeEmojis } from "../../../utils/textUtils"; interface WatermarkWordingProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx new file mode 100644 index 000000000..49b12c396 --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -0,0 +1,199 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + Text, + Stack, + Group, + TextInput, + Divider, + Modal +} from '@mantine/core'; +import CheckIcon from '@mui/icons-material/Check'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import ToolConfigurationModal from './ToolConfigurationModal'; +import ToolList from './ToolList'; +import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation'; +import { useAutomationForm } from '../../../hooks/tools/automate/useAutomationForm'; + + +interface AutomationCreationProps { + mode: AutomationMode; + existingAutomation?: AutomationConfig; + onBack: () => void; + onComplete: (automation: AutomationConfig) => void; + toolRegistry: Record; +} + +export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) { + const { t } = useTranslation(); + + const { + automationName, + setAutomationName, + selectedTools, + addTool, + removeTool, + updateTool, + hasUnsavedChanges, + canSaveAutomation, + getToolName, + getToolDefaultParameters + } = useAutomationForm({ mode, existingAutomation, toolRegistry }); + + const [configModalOpen, setConfigModalOpen] = useState(false); + const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1); + const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false); + + + const configureTool = (index: number) => { + setConfiguringToolIndex(index); + setConfigModalOpen(true); + }; + + const handleToolConfigSave = (parameters: Record) => { + if (configuraingToolIndex >= 0) { + updateTool(configuraingToolIndex, { + configured: true, + parameters + }); + } + setConfigModalOpen(false); + setConfiguringToolIndex(-1); + }; + + const handleToolConfigCancel = () => { + setConfigModalOpen(false); + setConfiguringToolIndex(-1); + }; + + const handleToolAdd = () => { + const newTool: AutomationTool = { + id: `tool-${Date.now()}`, + operation: '', + name: t('automate.creation.tools.selectTool', 'Select a tool...'), + configured: false, + parameters: {} + }; + updateTool(selectedTools.length, newTool); + }; + + const handleBackClick = () => { + if (hasUnsavedChanges()) { + setUnsavedWarningOpen(true); + } else { + onBack(); + } + }; + + const handleConfirmBack = () => { + setUnsavedWarningOpen(false); + onBack(); + }; + + const handleCancelBack = () => { + setUnsavedWarningOpen(false); + }; + + const saveAutomation = async () => { + if (!canSaveAutomation()) return; + + const automation = { + name: automationName.trim(), + description: '', + operations: selectedTools.map(tool => ({ + operation: tool.operation, + parameters: tool.parameters || {} + })) + }; + + try { + const { automationStorage } = await import('../../../services/automationStorage'); + const savedAutomation = await automationStorage.saveAutomation(automation); + onComplete(savedAutomation); + } catch (error) { + console.error('Error saving automation:', error); + } + }; + + const currentConfigTool = configuraingToolIndex >= 0 ? selectedTools[configuraingToolIndex] : null; + + return ( +
+ + {t("automate.creation.description", "Automations run tools sequentially. To get started, add tools in the order you want them to run.")} + + + + + {/* Automation Name */} + setAutomationName(e.currentTarget.value)} + size="sm" + /> + + + {/* Selected Tools List */} + {selectedTools.length > 0 && ( + + )} + + + + {/* Save Button */} + + + + {/* Tool Configuration Modal */} + {currentConfigTool && ( + + )} + + {/* Unsaved Changes Warning Modal */} + + + + {t('automate.creation.unsavedChanges.message', 'You have unsaved changes. Are you sure you want to go back? All changes will be lost.')} + + + + + + + +
+ ); +} diff --git a/frontend/src/components/tools/automate/AutomationEntry.tsx b/frontend/src/components/tools/automate/AutomationEntry.tsx new file mode 100644 index 000000000..3314831be --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationEntry.tsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; + +interface AutomationEntryProps { + /** Optional title for the automation (usually for custom ones) */ + title?: string; + /** MUI Icon component for the badge */ + badgeIcon?: React.ComponentType; + /** Array of tool operation names in the workflow */ + operations: string[]; + /** Click handler */ + onClick: () => void; + /** Whether to keep the icon at normal color (for special cases like "Add New") */ + keepIconColor?: boolean; + /** Show menu for saved/suggested automations */ + showMenu?: boolean; + /** Edit handler */ + onEdit?: () => void; + /** Delete handler */ + onDelete?: () => void; +} + +export default function AutomationEntry({ + title, + badgeIcon: BadgeIcon, + operations, + onClick, + keepIconColor = false, + showMenu = false, + onEdit, + onDelete +}: AutomationEntryProps) { + const { t } = useTranslation(); + const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + // Keep item in hovered state if menu is open + const shouldShowHovered = isHovered || isMenuOpen; + + const renderContent = () => { + if (title) { + // Custom automation with title + return ( + + {BadgeIcon && ( + + )} + + {title} + + + ); + } else { + // Suggested automation showing tool chain + return ( + + {BadgeIcon && ( + + )} + + {operations.map((op, index) => ( + + + {t(`${op}.title`, op)} + + + {index < operations.length - 1 && ( + + → + + )} + + ))} + + + ); + } + }; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + +
+ {renderContent()} +
+ + {showMenu && ( + setIsMenuOpen(true)} + onClose={() => setIsMenuOpen(false)} + > + + e.stopPropagation()} + style={{ + opacity: shouldShowHovered ? 1 : 0, + transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)', + transition: 'opacity 0.3s ease, transform 0.3s ease', + pointerEvents: shouldShowHovered ? 'auto' : 'none' + }} + > + + + + + + {onEdit && ( + } + onClick={(e) => { + e.stopPropagation(); + onEdit(); + }} + > + {t('edit', 'Edit')} + + )} + {onDelete && ( + } + onClick={(e) => { + e.stopPropagation(); + onDelete(); + }} + > + {t('delete', 'Delete')} + + )} + + + )} +
+
+ ); +} diff --git a/frontend/src/components/tools/automate/AutomationRun.tsx b/frontend/src/components/tools/automate/AutomationRun.tsx new file mode 100644 index 000000000..640f802f6 --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationRun.tsx @@ -0,0 +1,223 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import CheckIcon from "@mui/icons-material/Check"; +import { useFileSelection } from "../../../contexts/FileContext"; +import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry"; +import { AutomationConfig, ExecutionStep } from "../../../types/automation"; +import { AUTOMATION_CONSTANTS, EXECUTION_STATUS } from "../../../constants/automation"; +import { useResourceCleanup } from "../../../utils/resourceManager"; + +interface AutomationRunProps { + automation: AutomationConfig; + onComplete: () => void; + automateOperation?: any; // TODO: Type this properly when available +} + +export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) { + const { t } = useTranslation(); + const { selectedFiles } = useFileSelection(); + const toolRegistry = useFlatToolRegistry(); + const cleanup = useResourceCleanup(); + + // Progress tracking state + const [executionSteps, setExecutionSteps] = useState([]); + const [currentStepIndex, setCurrentStepIndex] = useState(-1); + + // Use the operation hook's loading state + const isExecuting = automateOperation?.isLoading || false; + const hasResults = automateOperation?.files.length > 0 || automateOperation?.downloadUrl !== null; + + // Initialize execution steps from automation + React.useEffect(() => { + if (automation?.operations) { + const steps = automation.operations.map((op: any, index: number) => { + const tool = toolRegistry[op.operation]; + return { + id: `${op.operation}-${index}`, + operation: op.operation, + name: tool?.name || op.operation, + status: EXECUTION_STATUS.PENDING + }; + }); + setExecutionSteps(steps); + setCurrentStepIndex(-1); + } + }, [automation, toolRegistry]); + + // Cleanup when component unmounts + React.useEffect(() => { + return () => { + // Reset progress state when component unmounts + setExecutionSteps([]); + setCurrentStepIndex(-1); + // Clean up any blob URLs + cleanup(); + }; + }, [cleanup]); + + const executeAutomation = async () => { + if (!selectedFiles || selectedFiles.length === 0) { + return; + } + + if (!automateOperation) { + console.error('No automateOperation provided'); + return; + } + + // Reset progress tracking + setCurrentStepIndex(0); + setExecutionSteps(prev => prev.map(step => ({ ...step, status: EXECUTION_STATUS.PENDING, error: undefined }))); + + try { + // Use the automateOperation.executeOperation to handle file consumption properly + await automateOperation.executeOperation( + { + automationConfig: automation, + onStepStart: (stepIndex: number, operationName: string) => { + setCurrentStepIndex(stepIndex); + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: EXECUTION_STATUS.RUNNING } : step + )); + }, + onStepComplete: (stepIndex: number, resultFiles: File[]) => { + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: EXECUTION_STATUS.COMPLETED } : step + )); + }, + onStepError: (stepIndex: number, error: string) => { + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: EXECUTION_STATUS.ERROR, error } : step + )); + } + }, + selectedFiles + ); + + // Mark all as completed and reset current step + setCurrentStepIndex(-1); + console.log(`✅ Automation completed successfully`); + } catch (error: any) { + console.error("Automation execution failed:", error); + setCurrentStepIndex(-1); + } + }; + + const getProgress = () => { + if (executionSteps.length === 0) return 0; + const completedSteps = executionSteps.filter(step => step.status === EXECUTION_STATUS.COMPLETED).length; + return (completedSteps / executionSteps.length) * 100; + }; + + const getStepIcon = (step: ExecutionStep) => { + switch (step.status) { + case EXECUTION_STATUS.COMPLETED: + return ; + case EXECUTION_STATUS.ERROR: + return ✕; + case EXECUTION_STATUS.RUNNING: + return
; + default: + return
; + } + }; + + return ( +
+ + {/* Automation Info */} + + + {automation?.name || t("automate.sequence.unnamed", "Unnamed Automation")} + + + {t("automate.sequence.steps", "{{count}} steps", { count: executionSteps.length })} + + + + {/* Progress Bar */} + {isExecuting && ( +
+ + Progress: {currentStepIndex + 1}/{executionSteps.length} + + +
+ )} + + {/* Execution Steps */} + + {executionSteps.map((step, index) => ( + + + {index + 1} + + + {getStepIcon(step)} + +
+ + {step.name} + + {step.error && ( + + {step.error} + + )} +
+
+ ))} +
+ + {/* Action Buttons */} + + + + {hasResults && ( + + )} + +
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/automate/AutomationSelection.tsx b/frontend/src/components/tools/automate/AutomationSelection.tsx new file mode 100644 index 000000000..f55cf4c5d --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationSelection.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Title, Stack, Divider } from "@mantine/core"; +import AddCircleOutline from "@mui/icons-material/AddCircleOutline"; +import SettingsIcon from "@mui/icons-material/Settings"; +import AutomationEntry from "./AutomationEntry"; +import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations"; +import { AutomationConfig } from "../../../types/automation"; + +interface AutomationSelectionProps { + savedAutomations: AutomationConfig[]; + onCreateNew: () => void; + onRun: (automation: AutomationConfig) => void; + onEdit: (automation: AutomationConfig) => void; + onDelete: (automation: AutomationConfig) => void; +} + +export default function AutomationSelection({ + savedAutomations, + onCreateNew, + onRun, + onEdit, + onDelete +}: AutomationSelectionProps) { + const { t } = useTranslation(); + const suggestedAutomations = useSuggestedAutomations(); + + return ( +
+ + {t("automate.selection.saved.title", "Saved")} + + + + + {/* Saved Automations */} + {savedAutomations.map((automation) => ( + typeof op === 'string' ? op : op.operation)} + onClick={() => onRun(automation)} + showMenu={true} + onEdit={() => onEdit(automation)} + onDelete={() => onDelete(automation)} + /> + ))} + + + {/* Suggested Automations */} +
+ + {t("automate.selection.suggested.title", "Suggested")} + + + {suggestedAutomations.map((automation) => ( + op.operation)} + onClick={() => onRun(automation)} + /> + ))} + +
+
+
+ ); +} diff --git a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx new file mode 100644 index 000000000..d97819fb8 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx @@ -0,0 +1,138 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Modal, + Title, + Button, + Group, + Stack, + Text, + Alert +} from '@mantine/core'; +import SettingsIcon from '@mui/icons-material/Settings'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import WarningIcon from '@mui/icons-material/Warning'; +import { ToolRegistry } from '../../../data/toolsTaxonomy'; +import { getAvailableToExtensions } from '../../../utils/convertUtils'; +interface ToolConfigurationModalProps { + opened: boolean; + tool: { + id: string; + operation: string; + name: string; + parameters?: any; + }; + onSave: (parameters: any) => void; + onCancel: () => void; + toolRegistry: ToolRegistry; +} + +export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) { + const { t } = useTranslation(); + + const [parameters, setParameters] = useState({}); + const [isValid, setIsValid] = useState(true); + + // Get tool info from registry + const toolInfo = toolRegistry[tool.operation]; + const SettingsComponent = toolInfo?.settingsComponent; + + // Initialize parameters from tool (which should contain defaults from registry) + useEffect(() => { + if (tool.parameters) { + setParameters(tool.parameters); + } else { + // Fallback to empty parameters if none provided + setParameters({}); + } + }, [tool.parameters, tool.operation]); + + // Render the settings component + const renderToolSettings = () => { + if (!SettingsComponent) { + return ( + } color="orange"> + + {t('automate.config.noSettings', 'This tool does not have configurable settings.')} + + + ); + } + + // Special handling for ConvertSettings which needs additional props + if (tool.operation === 'convert') { + return ( + { + setParameters((prev: any) => ({ ...prev, [key]: value })); + }} + getAvailableToExtensions={getAvailableToExtensions} + selectedFiles={[]} + disabled={false} + /> + ); + } + + return ( + { + setParameters((prev: any) => ({ ...prev, [key]: value })); + }} + disabled={false} + /> + ); + }; + + const handleSave = () => { + if (isValid) { + onSave(parameters); + } + }; + + return ( + + + + {t('automate.config.title', 'Configure {{toolName}}', { toolName: tool.name })} + + + } + size="lg" + centered + > + + + {t('automate.config.description', 'Configure the settings for this tool. These settings will be applied when the automation runs.')} + + +
+ {renderToolSettings()} +
+ + + + + +
+
+ ); +} diff --git a/frontend/src/components/tools/automate/ToolList.tsx b/frontend/src/components/tools/automate/ToolList.tsx new file mode 100644 index 000000000..8b24b5c17 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolList.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Text, Stack, Group, ActionIcon } from '@mantine/core'; +import DeleteIcon from '@mui/icons-material/Delete'; +import SettingsIcon from '@mui/icons-material/Settings'; +import CloseIcon from '@mui/icons-material/Close'; +import AddCircleOutline from '@mui/icons-material/AddCircleOutline'; +import { AutomationTool } from '../../../types/automation'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import ToolSelector from './ToolSelector'; +import AutomationEntry from './AutomationEntry'; + +interface ToolListProps { + tools: AutomationTool[]; + toolRegistry: Record; + onToolUpdate: (index: number, updates: Partial) => void; + onToolRemove: (index: number) => void; + onToolConfigure: (index: number) => void; + onToolAdd: () => void; + getToolName: (operation: string) => string; + getToolDefaultParameters: (operation: string) => Record; +} + +export default function ToolList({ + tools, + toolRegistry, + onToolUpdate, + onToolRemove, + onToolConfigure, + onToolAdd, + getToolName, + getToolDefaultParameters +}: ToolListProps) { + const { t } = useTranslation(); + + const handleToolSelect = (index: number, newOperation: string) => { + const defaultParams = getToolDefaultParameters(newOperation); + + onToolUpdate(index, { + operation: newOperation, + name: getToolName(newOperation), + configured: false, + parameters: defaultParams + }); + }; + + return ( +
+ + {t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length}) + + + {tools.map((tool, index) => ( + +
+ {/* Delete X in top right */} + onToolRemove(index)} + title={t('automate.creation.tools.remove', 'Remove tool')} + style={{ + position: 'absolute', + top: '4px', + right: '4px', + zIndex: 1, + color: 'var(--mantine-color-gray-6)' + }} + > + + + +
+ {/* Tool Selection Dropdown with inline settings cog */} + +
+ handleToolSelect(index, newOperation)} + excludeTools={['automate']} + toolRegistry={toolRegistry} + selectedValue={tool.operation} + placeholder={tool.name} + /> +
+ + {/* Settings cog - only show if tool is selected, aligned right */} + {tool.operation && ( + onToolConfigure(index)} + title={t('automate.creation.tools.configure', 'Configure tool')} + style={{ color: 'var(--mantine-color-gray-6)' }} + > + + + )} +
+ + {/* Configuration status underneath */} + {tool.operation && !tool.configured && ( + + {t('automate.creation.tools.notConfigured', "! Not Configured")} + + )} +
+
+ + {index < tools.length - 1 && ( +
+ ↓ +
+ )} +
+ ))} + + {/* Arrow before Add Tool Button */} + {tools.length > 0 && ( +
+ ↓ +
+ )} + + {/* Add Tool Button */} +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx new file mode 100644 index 000000000..80b68b0a4 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -0,0 +1,182 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Menu, Stack, Text, ScrollArea } from '@mantine/core'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import { useToolSections } from '../../../hooks/useToolSections'; +import { renderToolButtons } from '../shared/renderToolButtons'; +import ToolSearch from '../toolPicker/ToolSearch'; + +interface ToolSelectorProps { + onSelect: (toolKey: string) => void; + excludeTools?: string[]; + toolRegistry: Record; // Pass registry as prop to break circular dependency + selectedValue?: string; // For showing current selection when editing existing tool + placeholder?: string; // Custom placeholder text +} + +export default function ToolSelector({ + onSelect, + excludeTools = [], + toolRegistry, + selectedValue, + placeholder +}: ToolSelectorProps) { + const { t } = useTranslation(); + const [opened, setOpened] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + + // Filter out excluded tools (like 'automate' itself) + const baseFilteredTools = useMemo(() => { + return Object.entries(toolRegistry).filter(([key]) => !excludeTools.includes(key)); + }, [toolRegistry, excludeTools]); + + // Apply search filter + const filteredTools = useMemo(() => { + if (!searchTerm.trim()) { + return baseFilteredTools; + } + + const lowercaseSearch = searchTerm.toLowerCase(); + return baseFilteredTools.filter(([key, tool]) => { + return ( + tool.name.toLowerCase().includes(lowercaseSearch) || + tool.description?.toLowerCase().includes(lowercaseSearch) || + key.toLowerCase().includes(lowercaseSearch) + ); + }); + }, [baseFilteredTools, searchTerm]); + + // Create filtered tool registry for ToolSearch + const filteredToolRegistry = useMemo(() => { + const registry: Record = {}; + baseFilteredTools.forEach(([key, tool]) => { + registry[key] = tool; + }); + return registry; + }, [baseFilteredTools]); + + // Use the same tool sections logic as the main ToolPicker + const { sections, searchGroups } = useToolSections(filteredTools); + + // Determine what to display: search results or organized sections + const isSearching = searchTerm.trim().length > 0; + const displayGroups = useMemo(() => { + if (isSearching) { + return searchGroups || []; + } + + if (!sections || sections.length === 0) { + return []; + } + + // Find the "all" section which contains all tools without duplicates + const allSection = sections.find(s => (s as any).key === 'all'); + return allSection?.subcategories || []; + }, [isSearching, searchGroups, sections]); + + const handleToolSelect = useCallback((toolKey: string) => { + onSelect(toolKey); + setOpened(false); + setSearchTerm(''); // Clear search to show the selected tool display + }, [onSelect]); + + const renderedTools = useMemo(() => + displayGroups.map((subcategory) => + renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching) + ), [displayGroups, handleToolSelect, isSearching, t] + ); + + const handleSearchFocus = () => { + setOpened(true); + }; + + const handleSearchChange = (value: string) => { + setSearchTerm(value); + if (!opened) { + setOpened(true); + } + }; + + // Get display value for selected tool + const getDisplayValue = () => { + if (selectedValue && toolRegistry[selectedValue]) { + return toolRegistry[selectedValue].name; + } + return placeholder || t('automate.creation.tools.add', 'Add a tool...'); + }; + + return ( +
+ { + setOpened(isOpen); + // Clear search term when menu closes to show proper display + if (!isOpen) { + setSearchTerm(''); + } + }} + closeOnClickOutside={true} + closeOnEscape={true} + position="bottom-start" + offset={4} + withinPortal={false} + trapFocus={false} + shadow="sm" + transitionProps={{ duration: 0 }} + > + +
+ {selectedValue && toolRegistry[selectedValue] && !opened ? ( + // Show selected tool in AutomationEntry style when tool is selected and not searching +
+
+
+ {toolRegistry[selectedValue].icon} +
+ + {toolRegistry[selectedValue].name} + +
+
+ ) : ( + // Show search input when no tool selected or actively searching + + )} +
+
+ + + + + {displayGroups.length === 0 ? ( + + {isSearching + ? t('tools.noSearchResults', 'No tools found') + : t('tools.noTools', 'No tools available') + } + + ) : ( + renderedTools + )} + + + +
+
+ ); +} diff --git a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx index da0418571..fca3b5e56 100644 --- a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx +++ b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Stack, Text, Divider, Card, Group } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useSuggestedTools } from '../../../hooks/useSuggestedTools'; + export interface SuggestedToolsSectionProps {} export function SuggestedToolsSection(): React.ReactElement { diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index 3cb46ed60..ff6d2a6be 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -9,6 +9,7 @@ export interface FilesStepConfig { isCollapsed?: boolean; placeholder?: string; onCollapsedClick?: () => void; + isVisible?: boolean; } export interface MiddleStepConfig { @@ -63,7 +64,7 @@ export function createToolFlow(config: ToolFlowConfig) { {/* Files Step */} - {steps.createFilesStep({ + {config.files.isVisible !== false && steps.createFilesStep({ selectedFiles: config.files.selectedFiles, isCollapsed: config.files.isCollapsed, placeholder: config.files.placeholder, diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx new file mode 100644 index 000000000..eb9c9be6d --- /dev/null +++ b/frontend/src/components/tools/shared/renderToolButtons.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Box, Stack } from '@mantine/core'; +import ToolButton from '../toolPicker/ToolButton'; +import SubcategoryHeader from './SubcategoryHeader'; + +import { getSubcategoryLabel } from "../../../data/toolsTaxonomy"; +import { TFunction } from 'i18next'; +import { SubcategoryGroup } from '../../../hooks/useToolSections'; + +// Helper function to render tool buttons for a subcategory +export const renderToolButtons = ( + t: TFunction, + subcategory: SubcategoryGroup, + selectedToolKey: string | null, + onSelect: (id: string) => void, + showSubcategoryHeader: boolean = true +) => ( + + {showSubcategoryHeader && ( + + )} + + {subcategory.tools.map(({ id, tool }) => ( + + ))} + + +); diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index f01a9f87d..c17784a52 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -12,19 +12,26 @@ interface ToolSearchProps { onToolSelect?: (toolId: string) => void; mode: 'filter' | 'dropdown'; selectedToolKey?: string | null; + placeholder?: string; + hideIcon?: boolean; + onFocus?: () => void; } -const ToolSearch = ({ - value, - onChange, - toolRegistry, - onToolSelect, +const ToolSearch = ({ + value, + onChange, + toolRegistry, + onToolSelect, mode = 'filter', - selectedToolKey + selectedToolKey, + placeholder, + hideIcon = false, + onFocus }: ToolSearchProps) => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); const searchRef = useRef(null); + const dropdownRef = useRef(null); const filteredTools = useMemo(() => { if (!value.trim()) return []; @@ -47,7 +54,12 @@ const ToolSearch = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (searchRef.current && !searchRef.current.contains(event.target as Node)) { + if ( + searchRef.current && + dropdownRef.current && + !searchRef.current.contains(event.target as Node) && + !dropdownRef.current.contains(event.target as Node) + ) { setDropdownOpen(false); } }; @@ -61,9 +73,10 @@ const ToolSearch = ({ ref={searchRef} value={value} onChange={handleSearchChange} - placeholder={t("toolPicker.searchPlaceholder", "Search tools...")} - icon={search} + placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} + icon={hideIcon ? undefined : search} autoComplete="off" + />
); @@ -77,19 +90,19 @@ const ToolSearch = ({ {searchInput} {dropdownOpen && filteredTools.length > 0 && (
@@ -97,7 +110,10 @@ const ToolSearch = ({ - + - + + + +
{/* Hidden file input for native file picker */} Date: Mon, 25 Aug 2025 12:53:33 +0100 Subject: [PATCH 5/9] Feature/v2/right rail (#4255) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- .../public/locales/en-GB/translation.json | 14 + .../public/locales/en-US/translation.json | 13 + frontend/src/App.tsx | 3 + .../src/components/fileEditor/FileEditor.tsx | 54 +-- frontend/src/components/layout/Workbench.tsx | 1 + .../pageEditor/BulkSelectionPanel.tsx | 6 +- .../components/pageEditor/FileThumbnail.tsx | 2 - .../src/components/pageEditor/PageEditor.tsx | 126 +++--- .../pageEditor/PageEditorControls.tsx | 91 +---- .../components/pageEditor/PageThumbnail.tsx | 9 +- .../components/shared/LanguageSelector.tsx | 115 +++--- frontend/src/components/shared/RightRail.tsx | 385 ++++++++++++++++++ frontend/src/components/shared/Tooltip.tsx | 6 +- .../src/components/shared/TopControls.tsx | 151 +++---- .../shared/rightRail/RightRail.README.md | 108 +++++ .../components/shared/rightRail/RightRail.css | 127 ++++++ .../shared/tooltip/Tooltip.module.css | 2 +- frontend/src/components/tools/ToolPicker.tsx | 6 +- frontend/src/contexts/RightRailContext.tsx | 64 +++ frontend/src/hooks/useRightRailButtons.ts | 46 +++ frontend/src/pages/HomePage.tsx | 4 +- frontend/src/services/pdfExportService.ts | 28 +- frontend/src/styles/theme.css | 12 + frontend/src/types/rightRail.ts | 26 ++ 24 files changed, 1070 insertions(+), 329 deletions(-) create mode 100644 frontend/src/components/shared/RightRail.tsx create mode 100644 frontend/src/components/shared/rightRail/RightRail.README.md create mode 100644 frontend/src/components/shared/rightRail/RightRail.css create mode 100644 frontend/src/contexts/RightRailContext.tsx create mode 100644 frontend/src/hooks/useRightRailButtons.ts create mode 100644 frontend/src/types/rightRail.ts diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 256fab60e..2c9a0a6cd 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -992,6 +992,7 @@ }, "submit": "Change" }, + "removePages": { "tags": "Remove pages,delete pages", "title": "Remove Pages", @@ -1926,6 +1927,19 @@ "currentPage": "Current Page", "totalPages": "Total Pages" }, + "rightRail": { + "closeSelected": "Close Selected Files", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "selectByNumber": "Select by Page Numbers", + "deleteSelected": "Delete Selected Pages", + "closePdf": "Close PDF", + "exportAll": "Export PDF", + "downloadSelected": "Download Selected Files", + "downloadAll": "Download All", + "toggleTheme": "Toggle Theme", + "language": "Language" + }, "toolPicker": { "searchPlaceholder": "Search tools...", "noToolsFound": "No tools found", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index eb0483cbe..26c2e5b15 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -55,6 +55,7 @@ "bored": "Bored Waiting?", "alphabet": "Alphabet", "downloadPdf": "Download PDF", + "text": "Text", "font": "Font", "selectFillter": "-- Select --", @@ -2072,6 +2073,18 @@ } } }, + "rightRail": { + "closePdf": "Close PDF", + "closeSelected": "Close Selected Files", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "selectByNumber": "Select by Page Numbers", + "deleteSelected": "Delete Selected Pages", + "toggleTheme": "Toggle Theme", + "exportAll": "Export PDF", + "downloadSelected": "Download Selected Files", + "downloadAll": "Download All" + }, "removePassword": { "title": "Remove Password", "desc": "Remove password protection from your PDF document.", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b498b0677..ef4d663f6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import HomePage from "./pages/HomePage"; // Import global styles import "./styles/tailwind.css"; import "./index.css"; +import { RightRailProvider } from "./contexts/RightRailContext"; // Loading component for i18next suspense const LoadingFallback = () => ( @@ -38,7 +39,9 @@ export default function App() { + + diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index c93e78670..df1197ab9 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { - Button, Text, Center, Box, Notification, TextInput, LoadingOverlay, Modal, Alert, Container, - Stack, Group + Text, Center, Box, Notification, LoadingOverlay, Stack, Group, Portal } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useTranslation } from 'react-i18next'; @@ -466,21 +465,6 @@ const FileEditor = ({ - - {toolMode && ( - <> - - - - )} - {showBulkActions && !toolMode && ( - <> - - - )} - {activeFileRecords.length === 0 && !zipExtractionProgress.isExtracting ? ( @@ -573,25 +557,29 @@ const FileEditor = ({ /> {status && ( - setStatus(null)} - style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }} - > - {status} - + + setStatus(null)} + style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 10001 }} + > + {status} + + )} {error && ( - setError(null)} - style={{ position: 'fixed', bottom: 80, right: 20, zIndex: 1000 }} - > - {error} - + + setError(null)} + style={{ position: 'fixed', bottom: 80, right: 20, zIndex: 10001 }} + > + {error} + + )} diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index fc41d2480..a98b19b99 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -157,6 +157,7 @@ export default function Workbench() { className="flex-1 min-h-0 relative z-10" style={{ transition: 'opacity 0.15s ease-in-out', + marginTop: '1rem', }} > {renderMainContent()} diff --git a/frontend/src/components/pageEditor/BulkSelectionPanel.tsx b/frontend/src/components/pageEditor/BulkSelectionPanel.tsx index 5a6b4504f..b9ebb8d2c 100644 --- a/frontend/src/components/pageEditor/BulkSelectionPanel.tsx +++ b/frontend/src/components/pageEditor/BulkSelectionPanel.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Paper, Group, TextInput, Button, Text } from '@mantine/core'; +import { Group, TextInput, Button, Text } from '@mantine/core'; interface BulkSelectionPanelProps { csvInput: string; @@ -15,7 +15,7 @@ const BulkSelectionPanel = ({ onUpdatePagesFromCSV, }: BulkSelectionPanelProps) => { return ( - + <> )} - + ); }; diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx index d84eb2a16..609a31e1a 100644 --- a/frontend/src/components/pageEditor/FileThumbnail.tsx +++ b/frontend/src/components/pageEditor/FileThumbnail.tsx @@ -141,7 +141,6 @@ const FileThumbnail = ({ filter: isSupported ? 'none' : 'grayscale(50%)' }} > - {selectionMode && (
- )} {/* File content area */}
diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 7ca640b06..543778d9e 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -1,14 +1,13 @@ import React, { useState, useCallback, useRef, useEffect, useMemo } from "react"; import { - Button, Text, Center, Checkbox, Box, Tooltip, ActionIcon, + Button, Text, Center, Box, Notification, TextInput, LoadingOverlay, Modal, Alert, - Stack, Group + Stack, Group, Portal } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext"; import { ModeType } from "../../contexts/NavigationContext"; import { PDFDocument, PDFPage } from "../../types/pageEditor"; -import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing"; import { useUndoRedo } from "../../hooks/useUndoRedo"; import { RotatePagesCommand, @@ -56,7 +55,6 @@ export interface PageEditorProps { const PageEditor = ({ onFunctionsReady, }: PageEditorProps) => { - const { t } = useTranslation(); // Use split contexts to prevent re-renders const { state, selectors } = useFileState(); @@ -241,19 +239,26 @@ const PageEditor = ({ const [exportLoading, setExportLoading] = useState(false); const [showExportModal, setShowExportModal] = useState(false); const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null); + const [exportSelectedOnly, setExportSelectedOnly] = useState(false); // Animation state const [movingPage, setMovingPage] = useState(null); - const [pagePositions, setPagePositions] = useState>(new Map()); const [isAnimating, setIsAnimating] = useState(false); - const pageRefs = useRef>(new Map()); - const fileInputRef = useRef<() => void>(null); // Undo/Redo system const { executeCommand, undo, redo, canUndo, canRedo } = useUndoRedo(); + // Track whether the user has manually edited the filename to avoid auto-overwrites + const userEditedFilename = useRef(false); + + // Reset user edit flag when the active files change, so defaults can be applied for new docs + useEffect(() => { + userEditedFilename.current = false; + }, [filesSignature]); + // Set initial filename when document changes - use stable signature useEffect(() => { + if (userEditedFilename.current) return; // Do not overwrite user-typed filename if (mergedPdfDocument) { if (activeFileIds.length === 1 && primaryFileId) { const record = selectors.getFileRecord(primaryFileId); @@ -838,14 +843,18 @@ const PageEditor = ({ const handleDelete = useCallback(() => { if (!displayDocument) return; - const pagesToDelete = selectionMode - ? selectedPageNumbers.map(pageNum => { - const page = displayDocument.pages.find(p => p.pageNumber === pageNum); - return page?.id || ''; - }).filter(id => id) + const hasSelectedPages = selectedPageNumbers.length > 0; + + const pagesToDelete = (selectionMode || hasSelectedPages) + ? selectedPageNumbers + .map(pageNum => { + const page = displayDocument.pages.find(p => p.pageNumber === pageNum); + return page?.id || ''; + }) + .filter(id => id) : displayDocument.pages.map(p => p.id); - if (selectionMode && selectedPageNumbers.length === 0) return; + if ((selectionMode || hasSelectedPages) && selectedPageNumbers.length === 0) return; const command = new DeletePagesCommand( displayDocument, @@ -857,7 +866,7 @@ const PageEditor = ({ if (selectionMode) { actions.setSelectedPages([]); } - const pageCount = selectionMode ? selectedPageNumbers.length : displayDocument.pages.length; + const pageCount = (selectionMode || hasSelectedPages) ? selectedPageNumbers.length : displayDocument.pages.length; setStatus(`Deleted ${pageCount} pages`); }, [displayDocument, selectedPageNumbers, selectionMode, executeCommand, setPdfDocument, actions]); @@ -885,49 +894,52 @@ const PageEditor = ({ }, [displayDocument, selectedPageNumbers, selectionMode, executeCommand, setPdfDocument]); const showExportPreview = useCallback((selectedOnly: boolean = false) => { - if (!mergedPdfDocument) return; + const doc = editedDocument || mergedPdfDocument; + if (!doc) return; // Convert page numbers to page IDs for export service const exportPageIds = selectedOnly ? selectedPageNumbers.map(pageNum => { - const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); + const page = doc.pages.find(p => p.pageNumber === pageNum); return page?.id || ''; }).filter(id => id) : []; - - const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly); + const preview = pdfExportService.getExportInfo(doc, exportPageIds, selectedOnly); setExportPreview(preview); + setExportSelectedOnly(selectedOnly); setShowExportModal(true); - }, [mergedPdfDocument, selectedPageNumbers]); + }, [editedDocument, mergedPdfDocument, selectedPageNumbers]); const handleExport = useCallback(async (selectedOnly: boolean = false) => { - if (!mergedPdfDocument) return; + const doc = editedDocument || mergedPdfDocument; + if (!doc) return; setExportLoading(true); try { // Convert page numbers to page IDs for export service const exportPageIds = selectedOnly ? selectedPageNumbers.map(pageNum => { - const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); + const page = doc.pages.find(p => p.pageNumber === pageNum); return page?.id || ''; }).filter(id => id) : []; - const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly); + const errors = pdfExportService.validateExport(doc, exportPageIds, selectedOnly); if (errors.length > 0) { setStatus(errors.join(', ')); return; } - const hasSplitMarkers = mergedPdfDocument.pages.some(page => page.splitBefore); + const hasSplitMarkers = doc.pages.some(page => page.splitBefore); if (hasSplitMarkers) { - const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(doc, exportPageIds, { selectedOnly, filename, - splitDocuments: true + splitDocuments: true, + appendSuffix: false }) as { blobs: Blob[]; filenames: string[] }; result.blobs.forEach((blob, index) => { @@ -938,9 +950,10 @@ const PageEditor = ({ setStatus(`Exported ${result.blobs.length} split documents`); } else { - const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(doc, exportPageIds, { selectedOnly, - filename + filename, + appendSuffix: false }) as { blob: Blob; filename: string }; pdfExportService.downloadFile(result.blob, result.filename); @@ -953,7 +966,7 @@ const PageEditor = ({ } finally { setExportLoading(false); } - }, [mergedPdfDocument, selectedPageNumbers, filename]); + }, [editedDocument, mergedPdfDocument, selectedPageNumbers, filename]); const handleUndo = useCallback(() => { if (undo()) { @@ -1240,59 +1253,13 @@ const PageEditor = ({
)} - - setFilename(e.target.value)} placeholder="Enter filename" - style={{ minWidth: 200 }} + style={{ minWidth: 200, maxWidth: 200, marginLeft: "1rem"}} /> - - {selectionMode && ( - <> - - - - )} - - - {/* Apply Changes Button */} - {hasUnsavedChanges && ( - - )} - - - {selectionMode && ( - - )} - { setShowExportModal(false); - const selectedOnly = exportPreview.pageCount < (mergedPdfDocument?.pages.length || 0); - handleExport(selectedOnly); + handleExport(exportSelectedOnly); }} > Export PDF @@ -1446,14 +1412,16 @@ const PageEditor = ({ {status && ( + setStatus(null)} - style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }} + style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 10000 }} > {status} + )} {error && ( diff --git a/frontend/src/components/pageEditor/PageEditorControls.tsx b/frontend/src/components/pageEditor/PageEditorControls.tsx index 43e224e2b..2b0c6ee3c 100644 --- a/frontend/src/components/pageEditor/PageEditorControls.tsx +++ b/frontend/src/components/pageEditor/PageEditorControls.tsx @@ -2,16 +2,12 @@ import React from "react"; import { Tooltip, ActionIcon, - Paper } from "@mantine/core"; import UndoIcon from "@mui/icons-material/Undo"; import RedoIcon from "@mui/icons-material/Redo"; import ContentCutIcon from "@mui/icons-material/ContentCut"; -import DownloadIcon from "@mui/icons-material/Download"; import RotateLeftIcon from "@mui/icons-material/RotateLeft"; import RotateRightIcon from "@mui/icons-material/RotateRight"; -import DeleteIcon from "@mui/icons-material/Delete"; -import CloseIcon from "@mui/icons-material/Close"; interface PageEditorControlsProps { // Close/Reset functions @@ -39,17 +35,12 @@ interface PageEditorControlsProps { } const PageEditorControls = ({ - onClosePdf, onUndo, onRedo, canUndo, canRedo, onRotate, - onDelete, onSplit, - onExportSelected, - onExportAll, - exportLoading, selectionMode, selectedPages }: PageEditorControlsProps) => { @@ -57,9 +48,9 @@ const PageEditorControls = ({
- - {/* Close PDF */} - - - - - - -
{/* Undo/Redo */} @@ -133,17 +118,6 @@ const PageEditorControls = ({ - - 0 ? "light" : "default"} - size="lg" - > - - - -
- - {/* Export Controls */} - {selectionMode && selectedPages.length > 0 && ( - - - - - - )} - - - - - - +
); }; diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index f1590978a..7360b4dce 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -205,7 +205,7 @@ const PageThumbnail = React.memo(({ }} draggable={false} > - {selectionMode && ( + {
- )} + }
{ +interface LanguageSelectorProps { + position?: React.ComponentProps['position']; + offset?: number; + compact?: boolean; // icon-only trigger +} + +const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = false }: LanguageSelectorProps) => { const { i18n } = useTranslation(); const [opened, setOpened] = useState(false); const [animationTriggered, setAnimationTriggered] = useState(false); @@ -21,26 +27,27 @@ const LanguageSelector = () => { })); const handleLanguageChange = (value: string, event: React.MouseEvent) => { - // Create ripple effect at click position - const rect = event.currentTarget.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - - setRippleEffect({ x, y, key: Date.now() }); - + // Create ripple effect at click position (only for button mode) + if (!compact) { + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + setRippleEffect({ x, y, key: Date.now() }); + } + // Start transition animation setIsChanging(true); setPendingLanguage(value); - + // Simulate processing time for smooth transition setTimeout(() => { i18n.changeLanguage(value); - + setTimeout(() => { setIsChanging(false); setPendingLanguage(null); setOpened(false); - + // Clear ripple effect setTimeout(() => setRippleEffect(null), 100); }, 300); @@ -64,19 +71,9 @@ const LanguageSelector = () => { @@ -84,8 +81,8 @@ const LanguageSelector = () => { opened={opened} onChange={setOpened} width={600} - position="bottom-start" - offset={8} + position={position} + offset={offset} transitionProps={{ transition: 'scale-y', duration: 200, @@ -93,29 +90,45 @@ const LanguageSelector = () => { }} > - + }} + > + language + + ) : ( + + )} { }} > {option.label} - - {/* Ripple effect */} - {rippleEffect && pendingLanguage === option.value && ( + {!compact && rippleEffect && pendingLanguage === option.value && (
buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]); + + // Access PageEditor functions for page-editor-specific actions + const { pageEditorFunctions } = useToolWorkflow(); + + // CSV input state for page selection + const [csvInput, setCsvInput] = useState(""); + + // Navigation view + const { currentMode: currentView } = useNavigationState(); + + // File state and selection + const { state, selectors } = useFileState(); + const { selectedFiles, selectedFileIds, selectedPageNumbers, setSelectedFiles, setSelectedPages } = useFileSelection(); + const { removeFiles } = useFileManagement(); + + const activeFiles = selectors.getFiles(); + const filesSignature = selectors.getFilesSignature(); + const fileRecords = selectors.getFileRecords(); + + // Compute selection state and total items + const getSelectionState = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + const totalItems = activeFiles.length; + const selectedCount = selectedFileIds.length; + return { totalItems, selectedCount }; + } + + if (currentView === 'pageEditor') { + let totalItems = 0; + fileRecords.forEach(rec => { + const pf = rec.processedFile; + if (pf) { + totalItems += (pf.totalPages as number) || (pf.pages?.length || 0); + } + }); + const selectedCount = Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length : 0; + return { totalItems, selectedCount }; + } + + return { totalItems: 0, selectedCount: 0 }; + }, [currentView, activeFiles, fileRecords, selectedFileIds, selectedPageNumbers]); + + const { totalItems, selectedCount } = getSelectionState(); + + const handleSelectAll = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + // Select all file IDs + const allIds = state.files.ids; + setSelectedFiles(allIds); + return; + } + + if (currentView === 'pageEditor') { + let totalPages = 0; + fileRecords.forEach(rec => { + const pf = rec.processedFile; + if (pf) { + totalPages += (pf.totalPages as number) || (pf.pages?.length || 0); + } + }); + + if (totalPages > 0) { + setSelectedPages(Array.from({ length: totalPages }, (_, i) => i + 1)); + } + } + }, [currentView, state.files.ids, fileRecords, setSelectedFiles, setSelectedPages]); + + const handleDeselectAll = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + setSelectedFiles([]); + return; + } + if (currentView === 'pageEditor') { + setSelectedPages([]); + } + }, [currentView, setSelectedFiles, setSelectedPages]); + + const handleExportAll = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + // Download selected files (or all if none selected) + const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles; + + filesToDownload.forEach(file => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(file); + link.download = file.name; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); + }); + } else if (currentView === 'pageEditor') { + // Export all pages (not just selected) + pageEditorFunctions?.onExportAll?.(); + } + }, [currentView, activeFiles, selectedFiles, pageEditorFunctions]); + + const handleCloseSelected = useCallback(() => { + if (currentView !== 'fileEditor') return; + if (selectedFileIds.length === 0) return; + + // Close only selected files (do not delete from storage) + removeFiles(selectedFileIds, false); + + // Clear selection after closing + setSelectedFiles([]); + }, [currentView, selectedFileIds, removeFiles, setSelectedFiles]); + + // CSV parsing functions for page selection + const parseCSVInput = useCallback((csv: string) => { + const pageNumbers: number[] = []; + const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); + + ranges.forEach(range => { + if (range.includes('-')) { + const [start, end] = range.split('-').map(n => parseInt(n.trim())); + for (let i = start; i <= end; i++) { + if (i > 0) { + pageNumbers.push(i); + } + } + } else { + const pageNum = parseInt(range); + if (pageNum > 0) { + pageNumbers.push(pageNum); + } + } + }); + + return pageNumbers; + }, []); + + const updatePagesFromCSV = useCallback(() => { + const rawPages = parseCSVInput(csvInput); + // Determine max page count from processed records + const maxPages = fileRecords.reduce((sum, rec) => { + const pf = rec.processedFile; + if (!pf) return sum; + return sum + ((pf.totalPages as number) || (pf.pages?.length || 0)); + }, 0); + const normalized = Array.from(new Set(rawPages.filter(n => Number.isFinite(n) && n > 0 && n <= maxPages))).sort((a,b)=>a-b); + setSelectedPages(normalized); + }, [csvInput, parseCSVInput, fileRecords, setSelectedPages]); + + // Sync csvInput with selectedPageNumbers changes + useEffect(() => { + const sortedPageNumbers = Array.isArray(selectedPageNumbers) + ? [...selectedPageNumbers].sort((a, b) => a - b) + : []; + const newCsvInput = sortedPageNumbers.join(', '); + setCsvInput(newCsvInput); + }, [selectedPageNumbers]); + + // Clear CSV input when files change (use stable signature to avoid ref churn) + useEffect(() => { + setCsvInput(""); + }, [filesSignature]); + + // Mount/visibility for page-editor-only buttons to allow exit animation, then remove to avoid flex gap + const [pageControlsMounted, setPageControlsMounted] = useState(currentView === 'pageEditor'); + const [pageControlsVisible, setPageControlsVisible] = useState(currentView === 'pageEditor'); + + useEffect(() => { + if (currentView === 'pageEditor') { + // Mount and show + setPageControlsMounted(true); + // Next tick to ensure transition applies + requestAnimationFrame(() => setPageControlsVisible(true)); + } else { + // Start exit animation + setPageControlsVisible(false); + // After transition, unmount to remove flex gap + const timer = setTimeout(() => setPageControlsMounted(false), 240); + return () => clearTimeout(timer); + } + }, [currentView]); + + return ( +
+
+ {topButtons.length > 0 && ( + <> +
+ {topButtons.map(btn => ( + + actions[btn.id]?.()} + disabled={btn.disabled} + > + {btn.icon} + + + ))} +
+ + + )} + + {/* Group: Selection controls + Close, animate as one unit when entering/leaving viewer */} +
+
+ {/* Select All Button */} + +
+ + + select_all + + +
+
+ + {/* Deselect All Button */} + +
+ + + crop_square + + +
+
+ + {/* Select by Numbers - page editor only, with animated presence */} + {pageControlsMounted && ( + + +
+ + +
+ + + pin_end + + +
+
+ +
+ +
+
+
+
+
+ + )} + + {/* Delete Selected Pages - page editor only, with animated presence */} + {pageControlsMounted && ( + + +
+
+ { pageEditorFunctions?.handleDelete?.(); setSelectedPages([]); }} + disabled={!pageControlsVisible || (Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length === 0 : true)} + aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'} + > + delete + +
+
+
+ + )} + + {/* Close (File Editor: Close Selected | Page Editor: Close PDF) */} + +
+ pageEditorFunctions?.closePdf?.() : handleCloseSelected} + disabled={ + currentView === 'viewer' || + (currentView === 'fileEditor' && selectedCount === 0) || + (currentView === 'pageEditor' && (activeFiles.length === 0 || !pageEditorFunctions?.closePdf)) + } + > + + +
+
+
+ + +
+ + {/* Theme toggle and Language dropdown */} +
+ + + contrast + + + + + + 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All')) + } position="left" offset={12} arrow> +
+ + + download + + +
+
+
+ +
+
+
+ ); +} + + diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index c415eddf5..4c216d318 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -124,8 +124,8 @@ export const Tooltip: React.FC = ({ if (sidebarTooltip) return null; switch (position) { - case 'top': return "tooltip-arrow tooltip-arrow-top"; - case 'bottom': return "tooltip-arrow tooltip-arrow-bottom"; + case 'top': return "tooltip-arrow tooltip-arrow-bottom"; + case 'bottom': return "tooltip-arrow tooltip-arrow-top"; case 'left': return "tooltip-arrow tooltip-arrow-left"; case 'right': return "tooltip-arrow tooltip-arrow-right"; default: return "tooltip-arrow tooltip-arrow-right"; @@ -150,7 +150,7 @@ export const Tooltip: React.FC = ({ position: 'fixed', top: coords.top, left: coords.left, - width: (maxWidth !== undefined ? maxWidth : '25rem'), + width: (maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' : undefined)), minWidth: minWidth, zIndex: 9999, visibility: positionReady ? 'visible' : 'hidden', diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index ee5591694..229c3d362 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -1,23 +1,64 @@ -import React, { useState, useCallback, useMemo } from "react"; -import { Button, SegmentedControl, Loader } from "@mantine/core"; +import React, { useState, useCallback } from "react"; +import { SegmentedControl, Loader } from "@mantine/core"; import { useRainbowThemeContext } from "./RainbowThemeProvider"; -import LanguageSelector from "./LanguageSelector"; import rainbowStyles from '../../styles/rainbow.module.css'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LightModeIcon from '@mui/icons-material/LightMode'; -import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; import FolderIcon from "@mui/icons-material/Folder"; -import { Group } from "@mantine/core"; -import { ModeType } from '../../contexts/NavigationContext'; +import { ModeType, isValidMode } from '../../contexts/NavigationContext'; -// Stable view option objects that don't recreate on every render -const VIEW_OPTIONS_BASE = [ - { value: "viewer", icon: VisibilityIcon }, - { value: "pageEditor", icon: EditNoteIcon }, - { value: "fileEditor", icon: FolderIcon }, -] as const; +const viewOptionStyle = { + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', + gap: 6, + whiteSpace: 'nowrap', + paddingTop: '0.3rem', +} + + +// Create view options with icons and loading states +const createViewOptions = (switchingTo: ModeType | null) => [ + { + label: ( +
+ {switchingTo === "viewer" ? ( + + ) : ( + + )} + Read +
+ ), + value: "viewer", + }, + { + label: ( +
+ {switchingTo === "pageEditor" ? ( + + ) : ( + + )} + Page Editor +
+ ), + value: "pageEditor", + }, + { + label: ( +
+ {switchingTo === "fileEditor" ? ( + + ) : ( + + )} + File Manager +
+ ), + value: "fileEditor", + }, +]; interface TopControlsProps { currentView: ModeType; @@ -30,90 +71,60 @@ const TopControls = ({ setCurrentView, selectedToolKey, }: TopControlsProps) => { - const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext(); - const [switchingTo, setSwitchingTo] = useState(null); + const { isRainbowMode } = useRainbowThemeContext(); + const [switchingTo, setSwitchingTo] = useState(null); const isToolSelected = selectedToolKey !== null; const handleViewChange = useCallback((view: string) => { - // Guard against redundant changes - if (view === currentView) return; - + if (!isValidMode(view)) { + // Ignore invalid values defensively + return; + } + const mode = view as ModeType; + // Show immediate feedback - setSwitchingTo(view); + setSwitchingTo(mode as ModeType); // Defer the heavy view change to next frame so spinner can render requestAnimationFrame(() => { // Give the spinner one more frame to show requestAnimationFrame(() => { - setCurrentView(view as ModeType); - + setCurrentView(mode as ModeType); + // Clear the loading state after view change completes setTimeout(() => setSwitchingTo(null), 300); }); }); - }, [setCurrentView, currentView]); - - // Memoize the SegmentedControl data with stable references - const viewOptions = useMemo(() => - VIEW_OPTIONS_BASE.map(option => ({ - value: option.value, - label: ( - - {switchingTo === option.value ? ( - - ) : ( - - )} - - ) - })), [switchingTo]); - - const getThemeIcon = () => { - if (isRainbowMode) return ; - if (themeMode === "dark") return ; - return ; - }; + }, [setCurrentView]); return (
-
- - -
{!isToolSelected && ( -
+
diff --git a/frontend/src/components/shared/rightRail/RightRail.README.md b/frontend/src/components/shared/rightRail/RightRail.README.md new file mode 100644 index 000000000..7506e927c --- /dev/null +++ b/frontend/src/components/shared/rightRail/RightRail.README.md @@ -0,0 +1,108 @@ +# RightRail Component + +A dynamic vertical toolbar on the right side of the application that supports both static buttons (Undo/Redo, Save, Print, Share) and dynamic buttons registered by tools. + +## Structure + +- **Top Section**: Dynamic buttons from tools (empty when none) +- **Middle Section**: Grid, Cut, Undo, Redo +- **Bottom Section**: Save, Print, Share + +## Usage + +### For Tools (Recommended) + +```tsx +import { useRightRailButtons } from '../hooks/useRightRailButtons'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; + +function MyTool() { + const handleAction = useCallback(() => { + // Your action here + }, []); + + useRightRailButtons([ + { + id: 'my-action', + icon: , + tooltip: 'Execute Action', + onClick: handleAction, + }, + ]); + + return
My Tool
; +} +``` + +### Multiple Buttons + +```tsx +useRightRailButtons([ + { + id: 'primary', + icon: , + tooltip: 'Primary Action', + order: 1, + onClick: handlePrimary, + }, + { + id: 'secondary', + icon: , + tooltip: 'Secondary Action', + order: 2, + onClick: handleSecondary, + }, +]); +``` + +### Conditional Buttons + +```tsx +useRightRailButtons([ + // Always show + { + id: 'process', + icon: , + tooltip: 'Process', + disabled: isProcessing, + onClick: handleProcess, + }, + // Only show when condition met + ...(hasResults ? [{ + id: 'export', + icon: , + tooltip: 'Export', + onClick: handleExport, + }] : []), +]); +``` + +## API + +### Button Config + +```typescript +interface RightRailButtonWithAction { + id: string; // Unique identifier + icon: React.ReactNode; // Icon component + tooltip: string; // Hover tooltip + section?: 'top' | 'middle' | 'bottom'; // Section (default: 'top') + order?: number; // Sort order (default: 0) + disabled?: boolean; // Disabled state (default: false) + visible?: boolean; // Visibility (default: true) + onClick: () => void; // Click handler +} +``` + +## Built-in Features + +- **Undo/Redo**: Automatically integrates with Page Editor +- **Theme Support**: Light/dark mode with CSS variables +- **Auto Cleanup**: Buttons unregister when tool unmounts + +## Best Practices + +- Use descriptive IDs: `'compress-optimize'`, `'ocr-process'` +- Choose appropriate Material-UI icons +- Keep tooltips concise: `'Compress PDF'`, `'Process with OCR'` +- Use `useCallback` for click handlers to prevent re-registration diff --git a/frontend/src/components/shared/rightRail/RightRail.css b/frontend/src/components/shared/rightRail/RightRail.css new file mode 100644 index 000000000..8d01052a9 --- /dev/null +++ b/frontend/src/components/shared/rightRail/RightRail.css @@ -0,0 +1,127 @@ +.right-rail { + background-color: var(--right-rail-bg); + width: 3.5rem; + min-width: 3.5rem; + max-width: 3.5rem; + position: relative; + z-index: 10; + display: flex; + flex-direction: column; + height: 100vh; + border-left: 1px solid var(--border-subtle); +} + +.right-rail-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding: 1rem 0.5rem; +} + +.right-rail-section { + background-color: var(--right-rail-foreground); + border-radius: 12px; + padding: 0.5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; +} + +.right-rail-divider { + width: 2.75rem; + border: none; + border-top: 1px solid var(--tool-subcategory-rule-color); + margin: 0.25rem 0; +} + +.right-rail-icon { + color: var(--right-rail-icon); +} + +.right-rail-icon[aria-disabled="true"], +.right-rail-icon[disabled] { + color: var(--right-rail-icon-disabled) !important; + background-color: transparent !important; +} + +.right-rail-spacer { + flex: 1; +} + +/* Animated grow-down slot for buttons (mirrors current-tool-slot behavior) */ +.right-rail-slot { + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 450ms ease-out, opacity 300ms ease-out; +} + +.right-rail-enter { + animation: rightRailGrowDown 450ms ease-out; +} + +.right-rail-exit { + animation: rightRailShrinkUp 450ms ease-out; +} + +.right-rail-slot.visible { + max-height: 18rem; /* increased to fit additional controls + divider */ + opacity: 1; +} + +@keyframes rightRailGrowDown { + 0% { + max-height: 0; + opacity: 0; + } + 100% { + max-height: 18rem; + opacity: 1; + } +} + +@keyframes rightRailShrinkUp { + 0% { + max-height: 18rem; + opacity: 1; + } + 100% { + max-height: 0; + opacity: 0; + } +} + +/* Remove bottom margin from close icon */ +.right-rail-slot .right-rail-icon { + margin-bottom: 0; +} + +/* Inline appear/disappear animation for page-number selector button */ +.right-rail-fade { + transition-property: opacity, transform, max-height, visibility; + transition-duration: 220ms, 220ms, 220ms, 0s; + transition-timing-function: ease, ease, ease, linear; + transition-delay: 0s, 0s, 0s, 0s; + transform-origin: top center; + overflow: hidden; +} + +.right-rail-fade.enter { + opacity: 1; + transform: scale(1); + max-height: 3rem; + visibility: visible; +} + +.right-rail-fade.exit { + opacity: 0; + transform: scale(0.85); + max-height: 0; + visibility: hidden; + /* delay visibility change so opacity/max-height can finish */ + transition-delay: 0s, 0s, 0s, 220ms; + pointer-events: none; +} + diff --git a/frontend/src/components/shared/tooltip/Tooltip.module.css b/frontend/src/components/shared/tooltip/Tooltip.module.css index 46902c04b..50c242812 100644 --- a/frontend/src/components/shared/tooltip/Tooltip.module.css +++ b/frontend/src/components/shared/tooltip/Tooltip.module.css @@ -160,7 +160,7 @@ .tooltip-arrow-top { top: -0.25rem; left: 50%; - transform: translateX(-50%) rotate(45deg); + transform: translateX(-50%) rotate(-135deg); border-top: none; border-left: none; } diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index a8dbd7993..d81bf5ef0 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -85,7 +85,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa overflowY: "auto", overflowX: "hidden", minHeight: 0, - height: "100%" + height: "100%", + marginTop: -2 }} className="tool-picker-scrollable" > @@ -109,7 +110,6 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa zIndex: 2, borderTop: `0.0625rem solid var(--tool-header-border)`, borderBottom: `0.0625rem solid var(--tool-header-border)`, - marginBottom: -1, padding: "0.5rem 1rem", fontWeight: 700, background: "var(--tool-header-bg)", @@ -117,7 +117,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa cursor: "pointer", display: "flex", alignItems: "center", - justifyContent: "space-between" + justifyContent: "space-between", }} onClick={() => scrollTo(quickAccessRef)} > diff --git a/frontend/src/contexts/RightRailContext.tsx b/frontend/src/contexts/RightRailContext.tsx new file mode 100644 index 000000000..be3b7276c --- /dev/null +++ b/frontend/src/contexts/RightRailContext.tsx @@ -0,0 +1,64 @@ +import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import { RightRailAction, RightRailButtonConfig } from '../types/rightRail'; + +interface RightRailContextValue { + buttons: RightRailButtonConfig[]; + actions: Record; + registerButtons: (buttons: RightRailButtonConfig[]) => void; + unregisterButtons: (ids: string[]) => void; + setAction: (id: string, action: RightRailAction) => void; + clear: () => void; +} + +const RightRailContext = createContext(undefined); + +export function RightRailProvider({ children }: { children: React.ReactNode }) { + const [buttons, setButtons] = useState([]); + const [actions, setActions] = useState>({}); + + const registerButtons = useCallback((newButtons: RightRailButtonConfig[]) => { + setButtons(prev => { + const byId = new Map(prev.map(b => [b.id, b] as const)); + newButtons.forEach(nb => { + const existing = byId.get(nb.id) || ({} as RightRailButtonConfig); + byId.set(nb.id, { ...existing, ...nb }); + }); + const merged = Array.from(byId.values()); + merged.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id)); + if (process.env.NODE_ENV === 'development') { + const ids = newButtons.map(b => b.id); + const dupes = ids.filter((id, idx) => ids.indexOf(id) !== idx); + if (dupes.length) console.warn('[RightRail] Duplicate ids in registerButtons:', dupes); + } + return merged; + }); + }, []); + + const unregisterButtons = useCallback((ids: string[]) => { + setButtons(prev => prev.filter(b => !ids.includes(b.id))); + setActions(prev => Object.fromEntries(Object.entries(prev).filter(([id]) => !ids.includes(id)))); + }, []); + + const setAction = useCallback((id: string, action: RightRailAction) => { + setActions(prev => ({ ...prev, [id]: action })); + }, []); + + const clear = useCallback(() => { + setButtons([]); + setActions({}); + }, []); + + const value = useMemo(() => ({ buttons, actions, registerButtons, unregisterButtons, setAction, clear }), [buttons, actions, registerButtons, unregisterButtons, setAction, clear]); + + return ( + + {children} + + ); +} + +export function useRightRail() { + const ctx = useContext(RightRailContext); + if (!ctx) throw new Error('useRightRail must be used within RightRailProvider'); + return ctx; +} diff --git a/frontend/src/hooks/useRightRailButtons.ts b/frontend/src/hooks/useRightRailButtons.ts new file mode 100644 index 000000000..82a4e8cd5 --- /dev/null +++ b/frontend/src/hooks/useRightRailButtons.ts @@ -0,0 +1,46 @@ +import { useEffect, useMemo } from 'react'; +import { useRightRail } from '../contexts/RightRailContext'; +import { RightRailAction, RightRailButtonConfig } from '../types/rightRail'; + +export interface RightRailButtonWithAction extends RightRailButtonConfig { + onClick: RightRailAction; +} + +/** + * Registers one or more RightRail buttons and their actions. + * - Automatically registers on mount and unregisters on unmount + * - Updates registration when the input array reference changes + */ +export function useRightRailButtons(buttons: readonly RightRailButtonWithAction[]) { + const { registerButtons, unregisterButtons, setAction } = useRightRail(); + + // Memoize configs and ids to reduce churn + const configs: RightRailButtonConfig[] = useMemo( + () => buttons.map(({ onClick, ...cfg }) => cfg), + [buttons] + ); + const ids: string[] = useMemo(() => buttons.map(b => b.id), [buttons]); + + useEffect(() => { + if (!buttons || buttons.length === 0) return; + + // DEV warnings for duplicate ids or missing handlers + if (process.env.NODE_ENV === 'development') { + const idSet = new Set(); + buttons.forEach(b => { + if (!b.onClick) console.warn('[RightRail] Missing onClick for id:', b.id); + if (idSet.has(b.id)) console.warn('[RightRail] Duplicate id in buttons array:', b.id); + idSet.add(b.id); + }); + } + + // Register visual button configs (idempotent merge by id) + registerButtons(configs); + + // Bind/update actions independent of registration + buttons.forEach(({ id, onClick }) => setAction(id, onClick)); + + // Cleanup unregisters by ids present in this call + return () => unregisterButtons(ids); + }, [registerButtons, unregisterButtons, setAction, configs, ids, buttons]); +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index eeb23e83f..12c1f4d7f 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -9,6 +9,7 @@ import { getBaseUrl } from "../constants/app"; import ToolPanel from "../components/tools/ToolPanel"; import Workbench from "../components/layout/Workbench"; import QuickAccessBar from "../components/shared/QuickAccessBar"; +import RightRail from "../components/shared/RightRail"; import FileManager from "../components/FileManager"; @@ -46,7 +47,8 @@ export default function HomePage() { ref={quickAccessRef} /> + ); -} +} \ No newline at end of file diff --git a/frontend/src/services/pdfExportService.ts b/frontend/src/services/pdfExportService.ts index b0662437e..9345133b8 100644 --- a/frontend/src/services/pdfExportService.ts +++ b/frontend/src/services/pdfExportService.ts @@ -5,6 +5,7 @@ export interface ExportOptions { selectedOnly?: boolean; filename?: string; splitDocuments?: boolean; + appendSuffix?: boolean; // when false, do not append _edited/_selected } export class PDFExportService { @@ -16,7 +17,7 @@ export class PDFExportService { selectedPageIds: string[] = [], options: ExportOptions = {} ): Promise<{ blob: Blob; filename: string } | { blobs: Blob[]; filenames: string[] }> { - const { selectedOnly = false, filename, splitDocuments = false } = options; + const { selectedOnly = false, filename, splitDocuments = false, appendSuffix = true } = options; try { // Determine which pages to export @@ -36,7 +37,7 @@ export class PDFExportService { return await this.createSplitDocuments(sourceDoc, pagesToExport, filename || pdfDocument.name); } else { const blob = await this.createSingleDocument(sourceDoc, pagesToExport); - const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly); + const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly, appendSuffix); return { blob, filename: exportFilename }; } } catch (error) { @@ -56,7 +57,7 @@ export class PDFExportService { for (const page of pages) { // Get the original page from source document - const sourcePageIndex = page.pageNumber - 1; + const sourcePageIndex = this.getOriginalSourceIndex(page); if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) { // Copy the page @@ -113,7 +114,7 @@ export class PDFExportService { const newDoc = await PDFLibDocument.create(); for (const page of segmentPages) { - const sourcePageIndex = page.pageNumber - 1; + const sourcePageIndex = this.getOriginalSourceIndex(page); if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) { const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]); @@ -146,11 +147,28 @@ export class PDFExportService { return { blobs, filenames }; } + /** + * Derive the original page index from a page's stable id. + * Falls back to the current pageNumber if parsing fails. + */ + private getOriginalSourceIndex(page: PDFPage): number { + const match = page.id.match(/-page-(\d+)$/); + if (match) { + const originalNumber = parseInt(match[1], 10); + if (!Number.isNaN(originalNumber)) { + return originalNumber - 1; // zero-based index for pdf-lib + } + } + // Fallback to the visible page number + return Math.max(0, page.pageNumber - 1); + } + /** * Generate appropriate filename for export */ - private generateFilename(originalName: string, selectedOnly: boolean): string { + private generateFilename(originalName: string, selectedOnly: boolean, appendSuffix: boolean): string { const baseName = originalName.replace(/\.pdf$/i, ''); + if (!appendSuffix) return `${baseName}.pdf`; const suffix = selectedOnly ? '_selected' : '_edited'; return `${baseName}${suffix}.pdf`; } diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css index 634cae91c..a8efa179e 100644 --- a/frontend/src/styles/theme.css +++ b/frontend/src/styles/theme.css @@ -106,6 +106,12 @@ --icon-config-bg: #9CA3AF; --icon-config-color: #FFFFFF; + /* RightRail (light) */ + --right-rail-bg: #F5F6F8; /* light background */ + --right-rail-foreground: #CDD4E1; /* panel behind custom tool icons */ + --right-rail-icon: #4B5563; /* icon color */ + --right-rail-icon-disabled: #CECECE;/* disabled icon */ + /* Colors for tooltips */ --tooltip-title-bg: #DBEFFF; --tooltip-title-color: #31528E; @@ -234,6 +240,12 @@ --icon-inactive-bg: #2A2F36; --icon-inactive-color: #6E7581; + /* RightRail (dark) */ + --right-rail-bg: #1F2329; /* dark background */ + --right-rail-foreground: #2A2F36; /* panel behind custom tool icons */ + --right-rail-icon: #BCBEBF; /* icon color */ + --right-rail-icon-disabled: #43464B;/* disabled icon */ + /* Dark mode tooltip colors */ --tooltip-title-bg: #4B525A; --tooltip-title-color: #fff; diff --git a/frontend/src/types/rightRail.ts b/frontend/src/types/rightRail.ts new file mode 100644 index 000000000..1897a7170 --- /dev/null +++ b/frontend/src/types/rightRail.ts @@ -0,0 +1,26 @@ +import React from 'react'; + +export type RightRailSection = 'top' | 'middle' | 'bottom'; + +export interface RightRailButtonConfig { + /** Unique id for the button, also used to bind action callbacks */ + id: string; + /** Icon element to render */ + icon: React.ReactNode; + /** Tooltip content (can be localized node) */ + tooltip: React.ReactNode; + /** Optional ARIA label for a11y (separate from visual tooltip) */ + ariaLabel?: string; + /** Optional i18n key carried by config */ + templateKey?: string; + /** Visual grouping lane */ + section?: RightRailSection; + /** Sorting within a section (lower first); ties broken by id */ + order?: number; + /** Initial disabled state */ + disabled?: boolean; + /** Initial visibility */ + visible?: boolean; +} + +export type RightRailAction = () => void; From e6f4cfb3188f3be73dacfbec656623258db5bc05 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:10:13 +0100 Subject: [PATCH 6/9] Automate/v2/suggested (#4257) Suggested pipelines now work --------- Co-authored-by: Connor Yoh Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- .../tools/automate/useSuggestedAutomations.ts | 119 +++++++++++++++--- frontend/src/tools/Automate.tsx | 23 +++- 2 files changed, 119 insertions(+), 23 deletions(-) diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts index bb1ed5916..9ddce1e0b 100644 --- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts +++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts @@ -1,6 +1,9 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import StarIcon from '@mui/icons-material/Star'; +import CompressIcon from '@mui/icons-material/Compress'; +import SecurityIcon from '@mui/icons-material/Security'; +import TextFieldsIcon from '@mui/icons-material/TextFields'; import { SuggestedAutomation } from '../../../types/automation'; export function useSuggestedAutomations(): SuggestedAutomation[] { @@ -10,37 +13,119 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { const now = new Date().toISOString(); return [ { - id: "compress-and-merge", - name: t("automation.suggested.compressAndMerge", "Compress & Merge"), - description: t("automation.suggested.compressAndMergeDesc", "Compress PDFs and merge them into one file"), + id: "compress-and-split", + name: t("automation.suggested.compressAndSplit", "Compress & Split"), + description: t("automation.suggested.compressAndSplitDesc", "Compress PDFs and split them by pages"), operations: [ - { operation: "compress", parameters: {} }, - { operation: "merge", parameters: {} } + { + operation: "compress", + parameters: { + compressionLevel: 5, + grayscale: false, + expectedSize: '', + compressionMethod: 'quality', + fileSizeValue: '', + fileSizeUnit: 'MB', + } + }, + { + operation: "splitPdf", + parameters: { + mode: 'bySizeOrCount', + pages: '1', + hDiv: '2', + vDiv: '2', + merge: false, + splitType: 'pages', + splitValue: '1', + bookmarkLevel: '1', + includeMetadata: false, + allowDuplicates: false, + } + } ], createdAt: now, updatedAt: now, - icon: StarIcon, + icon: CompressIcon, }, { - id: "ocr-and-convert", - name: t("automation.suggested.ocrAndConvert", "OCR & Convert"), - description: t("automation.suggested.ocrAndConvertDesc", "Extract text via OCR and convert to different format"), + id: "ocr-workflow", + name: t("automation.suggested.ocrWorkflow", "OCR Processing"), + description: t("automation.suggested.ocrWorkflowDesc", "Extract text from PDFs using OCR technology"), operations: [ - { operation: "ocr", parameters: {} }, - { operation: "convert", parameters: {} } + { + operation: "ocr", + parameters: { + languages: ['eng'], + ocrType: 'skip-text', + ocrRenderType: 'hocr', + additionalOptions: [], + } + } ], createdAt: now, updatedAt: now, - icon: StarIcon, + icon: TextFieldsIcon, }, { id: "secure-workflow", - name: t("automation.suggested.secureWorkflow", "Secure Workflow"), - description: t("automation.suggested.secureWorkflowDesc", "Sanitize, add password, and set permissions"), + name: t("automation.suggested.secureWorkflow", "Security Workflow"), + description: t("automation.suggested.secureWorkflowDesc", "Sanitize PDFs and add password protection"), operations: [ - { operation: "sanitize", parameters: {} }, - { operation: "addPassword", parameters: {} }, - { operation: "changePermissions", parameters: {} } + { + operation: "sanitize", + parameters: { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false, + } + }, + { + operation: "addPassword", + parameters: { + password: 'password', + ownerPassword: '', + keyLength: 128, + permissions: { + preventAssembly: false, + preventExtractContent: false, + preventExtractForAccessibility: false, + preventFillInForm: false, + preventModify: false, + preventModifyAnnotations: false, + preventPrinting: false, + preventPrintingFaithful: false, + } + } + } + ], + createdAt: now, + updatedAt: now, + icon: SecurityIcon, + }, + { + id: "optimization-workflow", + name: t("automation.suggested.optimizationWorkflow", "Optimization Workflow"), + description: t("automation.suggested.optimizationWorkflowDesc", "Repair and compress PDFs for better performance"), + operations: [ + { + operation: "repair", + parameters: {} + }, + { + operation: "compress", + parameters: { + compressionLevel: 7, + grayscale: false, + expectedSize: '', + compressionMethod: 'quality', + fileSizeValue: '', + fileSizeUnit: 'MB', + } + } ], createdAt: now, updatedAt: now, diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 54538781b..af6b3d411 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -33,13 +33,19 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { if (currentStep === AUTOMATION_STEPS.RUN && data.step !== AUTOMATION_STEPS.RUN) { automateOperation.resetResults(); } - + + // If navigating to selection step, always clear results + if (data.step === AUTOMATION_STEPS.SELECTION) { + automateOperation.resetResults(); + automateOperation.clearError(); + } + // If navigating to run step with a different automation, reset results - if (data.step === AUTOMATION_STEPS.RUN && data.automation && + if (data.step === AUTOMATION_STEPS.RUN && data.automation && stepData.automation && data.automation.id !== stepData.automation.id) { automateOperation.resetResults(); } - + setStepData(data); setCurrentStep(data.step); }; @@ -47,7 +53,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const handleComplete = () => { // Reset automation results when completing automateOperation.resetResults(); - + // Reset to selection step setCurrentStep(AUTOMATION_STEPS.SELECTION); setStepData({ step: AUTOMATION_STEPS.SELECTION }); @@ -127,7 +133,12 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { createStep(t('automate.selection.title', 'Automation Selection'), { isVisible: true, isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION, - onCollapsedClick: () => setCurrentStep(AUTOMATION_STEPS.SELECTION) + onCollapsedClick: () => { + // Clear results when clicking back to selection + automateOperation.resetResults(); + setCurrentStep(AUTOMATION_STEPS.SELECTION); + setStepData({ step: AUTOMATION_STEPS.SELECTION }); + } }, currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null), createStep(stepData.mode === AutomationMode.EDIT @@ -158,7 +169,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }, steps: automationSteps, review: { - isVisible: hasResults, + isVisible: hasResults && currentStep === AUTOMATION_STEPS.RUN, operation: automateOperation, title: t('automate.reviewTitle', 'Automation Results') } From 55ebf9ebd07d9334db78ba7ad5af9395104e7253 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:46:44 +0100 Subject: [PATCH 7/9] Bug/v2/all tools section headers gap (#4275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes Screenshot 2025-08-24 at 2 42 22 PM to: Screenshot 2025-08-24 at 2 42 41 PM --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../src/components/shared/QuickAccessBar.tsx | 31 ++++++++++--------- frontend/src/components/tools/ToolPicker.tsx | 30 +++++++++++++++--- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 80ef86c83..704c2f9bf 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -54,21 +54,22 @@ const QuickAccessBar = forwardRef(({ handleReaderToggle(); } }, - { - id: 'sign', - name: t("quickAccess.sign", "Sign"), - icon: - - signature - , - size: 'lg', - isRound: false, - type: 'navigation', - onClick: () => { - setActiveButton('sign'); - handleToolSelect('sign'); - } - }, + // TODO: Add sign tool + // { + // id: 'sign', + // name: t("quickAccess.sign", "Sign"), + // icon: + // + // signature + // , + // size: 'lg', + // isRound: false, + // type: 'navigation', + // onClick: () => { + // setActiveButton('sign'); + // handleToolSelect('sign'); + // } + // }, { id: 'automate', name: t("quickAccess.automate", "Automate"), diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index d81bf5ef0..9a46c8a3e 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -25,19 +25,39 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa const quickAccessRef = useRef(null); const allToolsRef = useRef(null); - // On resize adjust headers height to offset height + // Keep header heights in sync with any dynamic size changes useLayoutEffect(() => { const update = () => { if (quickHeaderRef.current) { - setQuickHeaderHeight(quickHeaderRef.current.offsetHeight); + setQuickHeaderHeight(quickHeaderRef.current.offsetHeight || 0); } if (allHeaderRef.current) { - setAllHeaderHeight(allHeaderRef.current.offsetHeight); + setAllHeaderHeight(allHeaderRef.current.offsetHeight || 0); } }; + update(); + + // Update on window resize window.addEventListener("resize", update); - return () => window.removeEventListener("resize", update); + + // Update on element resize (e.g., font load, badge count change, zoom) + const observers: ResizeObserver[] = []; + if (typeof ResizeObserver !== "undefined") { + const observe = (el: HTMLDivElement | null, cb: () => void) => { + if (!el) return; + const ro = new ResizeObserver(() => cb()); + ro.observe(el); + observers.push(ro); + }; + observe(quickHeaderRef.current, update); + observe(allHeaderRef.current, update); + } + + return () => { + window.removeEventListener("resize", update); + observers.forEach(o => o.disconnect()); + }; }, []); const { sections: visibleSections } = useToolSections(filteredTools); @@ -152,7 +172,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa ref={allHeaderRef} style={{ position: "sticky", - top: quickSection ? quickHeaderHeight - 1: 0, + top: quickSection ? quickHeaderHeight -1 : 0, zIndex: 2, borderTop: `0.0625rem solid var(--tool-header-border)`, borderBottom: `0.0625rem solid var(--tool-header-border)`, From 73deece29efcacee1bd2181a38fb4359edcf31d0 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:07:55 +0100 Subject: [PATCH 8/9] V2 Replace Google Fonts icons with locally bundled Iconify icons (#4283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes This PR refactors the frontend icon system to remove reliance on @mui/icons-material and the Google Material Symbols webfont. 🔄 Changes Introduced a new LocalIcon component powered by Iconify. Added scripts/generate-icons.js to: Scan the codebase for used icons. Extract only required Material Symbols from @iconify-json/material-symbols. Generate a minimized JSON bundle and TypeScript types. Updated .gitignore to exclude generated icon files. Replaced all and MUI icon imports with usage. Removed material-symbols CSS import and related font dependency. Updated tsconfig.json to support JSON imports. Added prebuild/predev hooks to auto-generate the icons. ✅ Benefits No more 5MB+ Google webfont download → reduces initial page load size. Smaller install footprint → no giant @mui/icons-material dependency. Only ships the icons we actually use, cutting bundle size further. Type-safe icons via auto-generated MaterialSymbolIcon union type. Note most MUI not included in this update since they are low priority due to small SVG sizing (don't grab whole bundle) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: a --- frontend/.gitignore | 6 +- frontend/package-lock.json | 170 ++++++++++++++++- frontend/package.json | 8 +- frontend/scripts/generate-icons.js | 175 ++++++++++++++++++ .../src/components/shared/LandingPage.tsx | 6 +- .../components/shared/LanguageSelector.tsx | 6 +- frontend/src/components/shared/LocalIcon.tsx | 52 ++++++ .../src/components/shared/QuickAccessBar.tsx | 70 +++---- frontend/src/components/shared/RightRail.tsx | 24 +-- frontend/src/components/shared/TextInput.tsx | 3 +- frontend/src/components/shared/Tooltip.tsx | 5 +- .../src/components/tools/shared/ToolStep.tsx | 13 +- .../tools/shared/ToolWorkflowTitle.tsx | 5 +- .../tools/toolPicker/ToolSearch.tsx | 3 +- .../src/data/useTranslatedToolRegistry.tsx | 111 +++++------ frontend/src/global.d.ts | 13 +- .../tools/automate/useSuggestedAutomations.ts | 12 +- frontend/src/index.css | 6 - frontend/tsconfig.json | 2 +- 19 files changed, 535 insertions(+), 155 deletions(-) create mode 100644 frontend/scripts/generate-icons.js create mode 100644 frontend/src/components/shared/LocalIcon.tsx diff --git a/frontend/.gitignore b/frontend/.gitignore index 8b055b7a6..1191bbebf 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -24,4 +24,8 @@ yarn-debug.log* yarn-error.log* playwright-report -test-results \ No newline at end of file +test-results + +# auto-generated files +/src/assets/material-symbols-icons.json +/src/assets/material-symbols-icons.d.ts \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1438432a5..817f7b17e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@iconify/react": "^6.0.0", "@mantine/core": "^8.0.1", "@mantine/dropzone": "^8.0.1", "@mantine/hooks": "^8.0.1", @@ -29,7 +30,6 @@ "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", - "material-symbols": "^0.33.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", "react": "^19.1.0", @@ -40,6 +40,8 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@iconify-json/material-symbols": "^1.2.33", + "@iconify/utils": "^3.0.1", "@playwright/test": "^1.40.0", "@types/node": "^24.2.1", "@types/react": "^19.1.4", @@ -89,6 +91,28 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.0.tgz", + "integrity": "sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -1192,6 +1216,104 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@iconify-json/material-symbols": { + "version": "1.2.33", + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.33.tgz", + "integrity": "sha512-Bs0X1+/vpJydW63olrGh60zkR8/Y70sI14AIWaP7Z6YQXukzWANH4q3I0sIPklbIn1oL6uwLvh0zQyd6Vh79LQ==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/react": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@iconify/react/-/react-6.0.0.tgz", + "integrity": "sha512-eqNscABVZS8eCpZLU/L5F5UokMS9mnCf56iS1nM9YYHdH8ZxqZL9zyjSwW60IOQFsXZkilbBiv+1paMXBhSQnw==", + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.1.tgz", + "integrity": "sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==", + "dev": true, + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@antfu/utils": "^9.2.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.1", + "globals": "^15.15.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.1.1", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@iconify/utils/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/@iconify/utils/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -4446,6 +4568,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -5553,6 +5681,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true + }, "node_modules/license-checker": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", @@ -6097,12 +6231,6 @@ "semver": "bin/semver.js" } }, - "node_modules/material-symbols": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.33.0.tgz", - "integrity": "sha512-t9/Gz+14fClRgN7oVOt5CBuwsjFLxSNP9BRDyMrI5el3IZNvoD94IDGJha0YYivyAow24rCS0WOkAv4Dp+YjNg==", - "license": "Apache-2.0" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6653,6 +6781,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -7403,6 +7537,22 @@ "node": ">=6" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ] + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8615,6 +8765,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true + }, "node_modules/tinyglobby": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index cde323bcc..eaa5f20d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@iconify/react": "^6.0.0", "@mantine/core": "^8.0.1", "@mantine/dropzone": "^8.0.1", "@mantine/hooks": "^8.0.1", @@ -25,7 +26,6 @@ "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", - "material-symbols": "^0.33.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", "react": "^19.1.0", @@ -36,10 +36,14 @@ "web-vitals": "^2.1.4" }, "scripts": { + "predev": "npm run generate-icons", "dev": "npx tsc --noEmit && vite", + "prebuild": "npm run generate-icons", "build": "npx tsc --noEmit && vite build", "preview": "vite preview", "generate-licenses": "node scripts/generate-licenses.js", + "generate-icons": "node scripts/generate-icons.js", + "generate-icons:verbose": "node scripts/generate-icons.js --verbose", "test": "vitest", "test:watch": "vitest --watch", "test:coverage": "vitest --coverage", @@ -66,6 +70,8 @@ ] }, "devDependencies": { + "@iconify-json/material-symbols": "^1.2.33", + "@iconify/utils": "^3.0.1", "@playwright/test": "^1.40.0", "@types/node": "^24.2.1", "@types/react": "^19.1.4", diff --git a/frontend/scripts/generate-icons.js b/frontend/scripts/generate-icons.js new file mode 100644 index 000000000..681b06728 --- /dev/null +++ b/frontend/scripts/generate-icons.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +const { icons } = require('@iconify-json/material-symbols'); +const { getIcons } = require('@iconify/utils'); +const fs = require('fs'); +const path = require('path'); + +// Check for verbose flag +const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v'); + +// Logging functions +const info = (message) => console.log(message); +const debug = (message) => { + if (isVerbose) { + console.log(message); + } +}; + +// Function to scan codebase for LocalIcon usage +function scanForUsedIcons() { + const usedIcons = new Set(); + const srcDir = path.join(__dirname, '..', 'src'); + + info('🔍 Scanning codebase for LocalIcon usage...'); + + if (!fs.existsSync(srcDir)) { + console.error('❌ Source directory not found:', srcDir); + process.exit(1); + } + + // Recursively scan all .tsx and .ts files + function scanDirectory(dir) { + const files = fs.readdirSync(dir); + + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + scanDirectory(filePath); + } else if (file.endsWith('.tsx') || file.endsWith('.ts')) { + const content = fs.readFileSync(filePath, 'utf8'); + + // Match LocalIcon usage: + const localIconMatches = content.match(/]*icon="([^"]+)"/g); + if (localIconMatches) { + localIconMatches.forEach(match => { + const iconMatch = match.match(/icon="([^"]+)"/); + if (iconMatch) { + usedIcons.add(iconMatch[1]); + debug(` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); + } + }); + } + + // Match old material-symbols-rounded spans: icon-name + const spanMatches = content.match(/]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g); + if (spanMatches) { + spanMatches.forEach(match => { + const iconMatch = match.match(/>([^<]+)<\/span>/); + if (iconMatch && iconMatch[1].trim()) { + const iconName = iconMatch[1].trim(); + usedIcons.add(iconName); + debug(` Found (legacy): ${iconName} in ${path.relative(srcDir, filePath)}`); + } + }); + } + + // Match Icon component usage: + const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g); + if (iconMatches) { + iconMatches.forEach(match => { + const iconMatch = match.match(/icon="material-symbols:([^"]+)"/); + if (iconMatch) { + usedIcons.add(iconMatch[1]); + debug(` Found (Icon): ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); + } + }); + } + } + }); + } + + scanDirectory(srcDir); + + const iconArray = Array.from(usedIcons).sort(); + info(`📋 Found ${iconArray.length} unique icons across codebase`); + + return iconArray; +} + +// Auto-detect used icons +const usedIcons = scanForUsedIcons(); + +// Check if we need to regenerate (compare with existing) +const outputPath = path.join(__dirname, '..', 'src', 'assets', 'material-symbols-icons.json'); +let needsRegeneration = true; + +if (fs.existsSync(outputPath)) { + try { + const existingSet = JSON.parse(fs.readFileSync(outputPath, 'utf8')); + const existingIcons = Object.keys(existingSet.icons || {}).sort(); + const currentIcons = [...usedIcons].sort(); + + if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) { + needsRegeneration = false; + info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`); + } + } catch (error) { + // If we can't parse existing file, regenerate + needsRegeneration = true; + } +} + +if (!needsRegeneration) { + info('🎉 No regeneration needed!'); + process.exit(0); +} + +info(`🔍 Extracting ${usedIcons.length} icons from Material Symbols...`); + +// Extract only our used icons from the full set +const extractedIcons = getIcons(icons, usedIcons); + +if (!extractedIcons) { + console.error('❌ Failed to extract icons'); + process.exit(1); +} + +// Check for missing icons +const extractedIconNames = Object.keys(extractedIcons.icons || {}); +const missingIcons = usedIcons.filter(icon => !extractedIconNames.includes(icon)); + +if (missingIcons.length > 0) { + info(`âš ī¸ Missing icons (${missingIcons.length}): ${missingIcons.join(', ')}`); + info('💡 These icons don\'t exist in Material Symbols. Please use available alternatives.'); +} + +// Create output directory +const outputDir = path.join(__dirname, '..', 'src', 'assets'); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +// Write the extracted icon set to a file (outputPath already defined above) +fs.writeFileSync(outputPath, JSON.stringify(extractedIcons, null, 2)); + +info(`✅ Successfully extracted ${Object.keys(extractedIcons.icons || {}).length} icons`); +info(`đŸ“Ļ Bundle size: ${Math.round(JSON.stringify(extractedIcons).length / 1024)}KB`); +info(`💾 Saved to: ${outputPath}`); + +// Generate TypeScript types +const typesContent = `// Auto-generated icon types +// This file is automatically generated by scripts/generate-icons.js +// Do not edit manually - changes will be overwritten + +export type MaterialSymbolIcon = ${usedIcons.map(icon => `'${icon}'`).join(' | ')}; + +export interface IconSet { + prefix: string; + icons: Record; + width?: number; + height?: number; +} + +// Re-export the icon set as the default export with proper typing +declare const iconSet: IconSet; +export default iconSet; +`; + +const typesPath = path.join(outputDir, 'material-symbols-icons.d.ts'); +fs.writeFileSync(typesPath, typesContent); + +info(`📝 Generated types: ${typesPath}`); +info(`🎉 Icon extraction complete!`); \ No newline at end of file diff --git a/frontend/src/components/shared/LandingPage.tsx b/frontend/src/components/shared/LandingPage.tsx index 937e9cfbd..14322076e 100644 --- a/frontend/src/components/shared/LandingPage.tsx +++ b/frontend/src/components/shared/LandingPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Container, Text, Button, Checkbox, Group, useMantineColorScheme } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import AddIcon from '@mui/icons-material/Add'; +import LocalIcon from './LocalIcon'; import { useTranslation } from 'react-i18next'; import { useFileHandler } from '../../hooks/useFileHandler'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; @@ -138,7 +138,7 @@ const LandingPage = () => { onClick={handleOpenFilesModal} onMouseEnter={() => setIsUploadHover(false)} > - + {!isUploadHover && ( {t('landing.addFiles', 'Add Files')} @@ -165,7 +165,7 @@ const LandingPage = () => { onClick={handleNativeUploadClick} onMouseEnter={() => setIsUploadHover(true)} > - upload + {isUploadHover && ( {t('landing.uploadFromComputer', 'Upload from computer')} diff --git a/frontend/src/components/shared/LanguageSelector.tsx b/frontend/src/components/shared/LanguageSelector.tsx index 1d9e5b2dc..d3a346a8e 100644 --- a/frontend/src/components/shared/LanguageSelector.tsx +++ b/frontend/src/components/shared/LanguageSelector.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Menu, Button, ScrollArea, ActionIcon } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { supportedLanguages } from '../../i18n'; -import LanguageIcon from '@mui/icons-material/Language'; +import LocalIcon from './LocalIcon'; import styles from './LanguageSelector.module.css'; interface LanguageSelectorProps { @@ -105,13 +105,13 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal } }} > - language + ) : (
- {/* Add divider after Automate button (index 2) */} - {index === 2 && ( + {/* Add divider after Automate button (index 1) and Files button (index 2) */} + {index === 1 && ( - - select_all - +
@@ -251,9 +249,7 @@ export default function RightRail() { onClick={handleDeselectAll} disabled={currentView === 'viewer' || selectedCount === 0} > - - crop_square - +
@@ -273,9 +269,7 @@ export default function RightRail() { disabled={!pageControlsVisible || totalItems === 0} aria-label={typeof t === 'function' ? t('rightRail.selectByNumber', 'Select by Page Numbers') : 'Select by Page Numbers'} > - - pin_end - +
@@ -309,7 +303,7 @@ export default function RightRail() { disabled={!pageControlsVisible || (Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length === 0 : true)} aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'} > - delete +
@@ -331,7 +325,7 @@ export default function RightRail() { (currentView === 'pageEditor' && (activeFiles.length === 0 || !pageEditorFunctions?.closePdf)) } > - +
@@ -349,7 +343,7 @@ export default function RightRail() { className="right-rail-icon" onClick={toggleTheme} > - contrast + @@ -368,9 +362,7 @@ export default function RightRail() { onClick={handleExportAll} disabled={currentView === 'viewer' || totalItems === 0} > - - download - +
diff --git a/frontend/src/components/shared/TextInput.tsx b/frontend/src/components/shared/TextInput.tsx index e44e8efb2..fc3e99015 100644 --- a/frontend/src/components/shared/TextInput.tsx +++ b/frontend/src/components/shared/TextInput.tsx @@ -1,5 +1,6 @@ import React, { forwardRef } from 'react'; import { useMantineColorScheme } from '@mantine/core'; +import LocalIcon from './LocalIcon'; import styles from './textInput/TextInput.module.css'; /** @@ -96,7 +97,7 @@ export const TextInput = forwardRef(({ style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }} aria-label="Clear input" > - close + )}
diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index 4c216d318..7940112ca 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; +import LocalIcon from './LocalIcon'; import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils'; import { useTooltipPosition } from '../../hooks/useTooltipPosition'; import { TooltipTip } from '../../types/tips'; @@ -171,9 +172,7 @@ export const Tooltip: React.FC = ({ }} title="Close tooltip" > - - close - + )} {arrow && getArrowClass() && ( diff --git a/frontend/src/components/tools/shared/ToolStep.tsx b/frontend/src/components/tools/shared/ToolStep.tsx index c2ff1d5f6..9d86d2f03 100644 --- a/frontend/src/components/tools/shared/ToolStep.tsx +++ b/frontend/src/components/tools/shared/ToolStep.tsx @@ -1,7 +1,6 @@ import React, { createContext, useContext, useMemo, useRef } from 'react'; import { Text, Stack, Box, Flex, Divider } from '@mantine/core'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import LocalIcon from '../../shared/LocalIcon'; import { Tooltip } from '../../shared/Tooltip'; import { TooltipTip } from '../../../types/tips'; import { createFilesToolStep, FilesToolStepProps } from './FilesToolStep'; @@ -54,9 +53,7 @@ const renderTooltipTitle = ( {title} - - gpp_maybe - + ); @@ -125,14 +122,12 @@ const ToolStep = ({ {isCollapsed ? ( - ) : ( - diff --git a/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx index 31c305d4a..6ed949442 100644 --- a/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx +++ b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Flex, Text, Divider } from '@mantine/core'; +import LocalIcon from '../../shared/LocalIcon'; import { Tooltip } from '../../shared/Tooltip'; export interface ToolWorkflowTitleProps { @@ -29,9 +30,7 @@ export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) { {title} - - gpp_maybe - + diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index c17784a52..774126aa2 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect, useMemo } from "react"; import { Stack, Button, Text } from "@mantine/core"; import { useTranslation } from "react-i18next"; +import LocalIcon from '../../shared/LocalIcon'; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; import { TextInput } from "../../shared/TextInput"; import './ToolPicker.css'; @@ -74,7 +75,7 @@ const ToolSearch = ({ value={value} onChange={handleSearchChange} placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} - icon={hideIcon ? undefined : search} + icon={hideIcon ? undefined : } autoComplete="off" /> diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 3c3b5e89b..db8b2cf23 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import LocalIcon from '../components/shared/LocalIcon'; import { useTranslation } from 'react-i18next'; import SplitPdfPanel from "../tools/Split"; import CompressPdfPanel from "../tools/Compress"; @@ -50,7 +51,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Signing "certSign": { - icon: workspace_premium, + icon: , name: t("home.certSign.title", "Sign with Certificate"), component: null, view: "sign", @@ -59,7 +60,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.SIGNING }, "sign": { - icon: signature, + icon: , name: t("home.sign.title", "Sign"), component: null, view: "sign", @@ -72,7 +73,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Document Security "addPassword": { - icon: password, + icon: , name: t("home.addPassword.title", "Add Password"), component: AddPassword, view: "security", @@ -85,7 +86,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: AddPasswordSettings }, "watermark": { - icon: branding_watermark, + icon: , name: t("home.watermark.title", "Add Watermark"), component: AddWatermark, view: "format", @@ -98,7 +99,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: AddWatermarkSingleStepSettings }, "add-stamp": { - icon: approval, + icon: , name: t("home.AddStampRequest.title", "Add Stamp to PDF"), component: null, view: "format", @@ -107,7 +108,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "sanitize": { - icon: cleaning_services, + icon: , name: t("home.sanitize.title", "Sanitize"), component: Sanitize, view: "security", @@ -120,7 +121,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: SanitizeSettings }, "flatten": { - icon: layers_clear, + icon: , name: t("home.flatten.title", "Flatten"), component: null, view: "format", @@ -129,7 +130,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "unlock-pdf-forms": { - icon: preview_off, + icon: , name: t("home.unlockPDFForms.title", "Unlock PDF Forms"), component: UnlockPdfForms, view: "security", @@ -142,7 +143,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: UnlockPdfFormsSettings }, "manage-certificates": { - icon: license, + icon: , name: t("home.manageCertificates.title", "Manage Certificates"), component: null, view: "security", @@ -151,7 +152,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "change-permissions": { - icon: lock, + icon: , name: t("home.changePermissions.title", "Change Permissions"), component: ChangePermissions, view: "security", @@ -166,7 +167,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Verification "get-all-info-on-pdf": { - icon: fact_check, + icon: , name: t("home.getPdfInfo.title", "Get ALL Info on PDF"), component: null, view: "extract", @@ -175,7 +176,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.VERIFICATION }, "validate-pdf-signature": { - icon: verified, + icon: , name: t("home.validateSignature.title", "Validate PDF Signature"), component: null, view: "security", @@ -188,7 +189,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Document Review "read": { - icon: article, + icon: , name: t("home.read.title", "Read"), component: null, view: "view", @@ -197,7 +198,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.DOCUMENT_REVIEW }, "change-metadata": { - icon: assignment, + icon: , name: t("home.changeMetadata.title", "Change Metadata"), component: null, view: "format", @@ -208,7 +209,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Page Formatting "cropPdf": { - icon: crop, + icon: , name: t("home.crop.title", "Crop PDF"), component: null, view: "format", @@ -217,7 +218,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.PAGE_FORMATTING }, "rotate": { - icon: rotate_right, + icon: , name: t("home.rotate.title", "Rotate"), component: null, view: "format", @@ -226,7 +227,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.PAGE_FORMATTING }, "splitPdf": { - icon: content_cut, + icon: , name: t("home.split.title", "Split"), component: SplitPdfPanel, view: "split", @@ -237,7 +238,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: SplitSettings }, "reorganize-pages": { - icon: move_down, + icon: , name: t("home.reorganizePages.title", "Reorganize Pages"), component: null, view: "pageEditor", @@ -246,7 +247,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.PAGE_FORMATTING }, "adjust-page-size-scale": { - icon: crop_free, + icon: , name: t("home.scalePages.title", "Adjust page size/scale"), component: null, view: "format", @@ -255,7 +256,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.PAGE_FORMATTING }, "addPageNumbers": { - icon: 123, + icon: , name: t("home.addPageNumbers.title", "Add Page Numbers"), component: null, view: "format", @@ -264,7 +265,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.PAGE_FORMATTING }, "multi-page-layout": { - icon: dashboard, + icon: , name: t("home.pageLayout.title", "Multi-Page Layout"), component: null, view: "format", @@ -273,7 +274,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.PAGE_FORMATTING }, "single-large-page": { - icon: looks_one, + icon: , name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"), component: SingleLargePage, view: "format", @@ -285,7 +286,7 @@ export function useFlatToolRegistry(): ToolRegistry { operationConfig: singleLargePageOperationConfig }, "add-attachments": { - icon: attachment, + icon: , name: t("home.attachments.title", "Add Attachments"), component: null, view: "format", @@ -298,7 +299,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Extraction "extractPages": { - icon: upload, + icon: , name: t("home.extractPages.title", "Extract Pages"), component: null, view: "extract", @@ -307,7 +308,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.EXTRACTION }, "extract-images": { - icon: filter, + icon: , name: t("home.extractImages.title", "Extract Images"), component: null, view: "extract", @@ -320,7 +321,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Removal "removePages": { - icon: delete, + icon: , name: t("home.removePages.title", "Remove Pages"), component: null, view: "remove", @@ -329,7 +330,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.REMOVAL }, "remove-blank-pages": { - icon: scan_delete, + icon: , name: t("home.removeBlanks.title", "Remove Blank Pages"), component: null, view: "remove", @@ -338,7 +339,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.REMOVAL }, "remove-annotations": { - icon: thread_unread, + icon: , name: t("home.removeAnnotations.title", "Remove Annotations"), component: null, view: "remove", @@ -347,7 +348,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.REMOVAL }, "remove-image": { - icon: remove_selection, + icon: , name: t("home.removeImagePdf.title", "Remove Image"), component: null, view: "format", @@ -356,7 +357,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.REMOVAL }, "remove-password": { - icon: lock_open_right, + icon: , name: t("home.removePassword.title", "Remove Password"), component: RemovePassword, view: "security", @@ -369,7 +370,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: RemovePasswordSettings }, "remove-certificate-sign": { - icon: remove_moderator, + icon: , name: t("home.removeCertSign.title", "Remove Certificate Sign"), component: RemoveCertificateSign, view: "security", @@ -385,7 +386,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Automation "automate": { - icon: automation, + icon: , name: t("home.automate.title", "Automate"), component: React.lazy(() => import('../tools/Automate')), view: "format", @@ -396,7 +397,7 @@ export function useFlatToolRegistry(): ToolRegistry { endpoints: ["handleData"] }, "auto-rename-pdf-file": { - icon: match_word, + icon: , name: t("home.auto-rename.title", "Auto Rename PDF File"), component: null, view: "format", @@ -405,7 +406,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.AUTOMATION }, "auto-split-pages": { - icon: split_scene_right, + icon: , name: t("home.autoSplitPDF.title", "Auto Split Pages"), component: null, view: "format", @@ -414,7 +415,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.AUTOMATION }, "auto-split-by-size-count": { - icon: content_cut, + icon: , name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"), component: null, view: "format", @@ -427,7 +428,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Advanced Formatting "adjustContrast": { - icon: palette, + icon: , name: t("home.adjustContrast.title", "Adjust Colors/Contrast"), component: null, view: "format", @@ -436,7 +437,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "repair": { - icon: build, + icon: , name: t("home.repair.title", "Repair"), component: Repair, view: "format", @@ -449,7 +450,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: RepairSettings }, "detect-split-scanned-photos": { - icon: scanner, + icon: , name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"), component: null, view: "format", @@ -458,7 +459,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "overlay-pdfs": { - icon: layers, + icon: , name: t("home.overlay-pdfs.title", "Overlay PDFs"), component: null, view: "format", @@ -467,7 +468,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "replace-and-invert-color": { - icon: format_color_fill, + icon: , name: t("home.replaceColorPdf.title", "Replace & Invert Color"), component: null, view: "format", @@ -476,7 +477,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "add-image": { - icon: image, + icon: , name: t("home.addImage.title", "Add Image"), component: null, view: "format", @@ -485,7 +486,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "edit-table-of-contents": { - icon: bookmark_add, + icon: , name: t("home.editTableOfContents.title", "Edit Table of Contents"), component: null, view: "format", @@ -494,7 +495,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "scanner-effect": { - icon: scanner, + icon: , name: t("home.fakeScan.title", "Scanner Effect"), component: null, view: "format", @@ -507,7 +508,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Developer Tools "show-javascript": { - icon: javascript, + icon: , name: t("home.showJS.title", "Show JavaScript"), component: null, view: "extract", @@ -516,7 +517,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.DEVELOPER_TOOLS }, "dev-api": { - icon: open_in_new, + icon: , name: t("home.devApi.title", "API"), component: null, view: "external", @@ -526,7 +527,7 @@ export function useFlatToolRegistry(): ToolRegistry { link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html" }, "dev-folder-scanning": { - icon: open_in_new, + icon: , name: t("home.devFolderScanning.title", "Automated Folder Scanning"), component: null, view: "external", @@ -536,7 +537,7 @@ export function useFlatToolRegistry(): ToolRegistry { link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/" }, "dev-sso-guide": { - icon: open_in_new, + icon: , name: t("home.devSsoGuide.title", "SSO Guide"), component: null, view: "external", @@ -546,7 +547,7 @@ export function useFlatToolRegistry(): ToolRegistry { link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration", }, "dev-airgapped": { - icon: open_in_new, + icon: , name: t("home.devAirgapped.title", "Air-gapped Setup"), component: null, view: "external", @@ -559,7 +560,7 @@ export function useFlatToolRegistry(): ToolRegistry { // Recommended Tools "compare": { - icon: compare, + icon: , name: t("home.compare.title", "Compare"), component: null, view: "format", @@ -568,7 +569,7 @@ export function useFlatToolRegistry(): ToolRegistry { subcategoryId: SubcategoryId.GENERAL }, "compress": { - icon: zoom_in_map, + icon: , name: t("home.compress.title", "Compress"), component: CompressPdfPanel, view: "compress", @@ -580,7 +581,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: CompressSettings }, "convert": { - icon: sync_alt, + icon: , name: t("home.convert.title", "Convert"), component: ConvertPanel, view: "convert", @@ -626,7 +627,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: ConvertSettings }, "mergePdfs": { - icon: library_add, + icon: , name: t("home.merge.title", "Merge"), component: null, view: "merge", @@ -636,7 +637,7 @@ export function useFlatToolRegistry(): ToolRegistry { maxFiles: -1 }, "multi-tool": { - icon: dashboard_customize, + icon: , name: t("home.multiTool.title", "Multi-Tool"), component: null, view: "pageEditor", @@ -646,7 +647,7 @@ export function useFlatToolRegistry(): ToolRegistry { maxFiles: -1 }, "ocr": { - icon: quick_reference_all, + icon: , name: t("home.ocr.title", "OCR"), component: OCRPanel, view: "convert", @@ -658,7 +659,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: OCRSettings }, "redact": { - icon: visibility_off, + icon: , name: t("home.redact.title", "Redact"), component: null, view: "redact", diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts index eb4b5d6c2..5511059a8 100644 --- a/frontend/src/global.d.ts +++ b/frontend/src/global.d.ts @@ -4,4 +4,15 @@ declare module "../components/PageEditor"; declare module "../components/Viewer"; declare module "*.js"; declare module '*.module.css'; -declare module 'pdfjs-dist'; \ No newline at end of file +declare module 'pdfjs-dist'; + +// Auto-generated icon set JSON import +declare module '../assets/material-symbols-icons.json' { + const value: { + prefix: string; + icons: Record; + width?: number; + height?: number; + }; + export default value; +} \ No newline at end of file diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts index 9ddce1e0b..006c9f179 100644 --- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts +++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts @@ -1,11 +1,15 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import StarIcon from '@mui/icons-material/Star'; -import CompressIcon from '@mui/icons-material/Compress'; -import SecurityIcon from '@mui/icons-material/Security'; -import TextFieldsIcon from '@mui/icons-material/TextFields'; +import React from 'react'; +import LocalIcon from '../../../components/shared/LocalIcon'; import { SuggestedAutomation } from '../../../types/automation'; +// Create icon components +const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' }); +const TextFieldsIcon = () => React.createElement(LocalIcon, { icon: 'text-fields', width: '1.5rem', height: '1.5rem' }); +const SecurityIcon = () => React.createElement(LocalIcon, { icon: 'security', width: '1.5rem', height: '1.5rem' }); +const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1.5rem', height: '1.5rem' }); + export function useSuggestedAutomations(): SuggestedAutomation[] { const { t } = useTranslation(); diff --git a/frontend/src/index.css b/frontend/src/index.css index f7e5e0865..ec2585e8c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,9 +1,3 @@ -@import 'material-symbols/rounded.css'; - -.material-symbols-rounded { - font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; -} - body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 215a9378b..6886183a1 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -42,7 +42,7 @@ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ From ed61c71db7badd5cc7985db57dd1a33305aa70cc Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:10:51 +0100 Subject: [PATCH 9/9] Update Frontend 3rd Party Licenses (#4254) Auto-generated by stirlingbot[bot] This PR updates the frontend license report based on changes to package.json dependencies. Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- frontend/src/assets/3rdPartyLicenses.json | 62 +++++++++++++++++++---- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/frontend/src/assets/3rdPartyLicenses.json b/frontend/src/assets/3rdPartyLicenses.json index 0235380af..2f19f5db6 100644 --- a/frontend/src/assets/3rdPartyLicenses.json +++ b/frontend/src/assets/3rdPartyLicenses.json @@ -21,6 +21,13 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "@atlaskit/pragmatic-drag-and-drop", + "moduleUrl": "https://github.com/atlassian/pragmatic-drag-and-drop", + "moduleVersion": "1.7.4", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "@babel/code-frame", "moduleUrl": "https://github.com/babel/babel", @@ -59,7 +66,7 @@ { "moduleName": "@babel/parser", "moduleUrl": "https://github.com/babel/babel", - "moduleVersion": "7.27.3", + "moduleVersion": "7.28.3", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -87,7 +94,7 @@ { "moduleName": "@babel/types", "moduleUrl": "https://github.com/babel/babel", - "moduleVersion": "7.27.3", + "moduleVersion": "7.28.2", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -217,6 +224,20 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "@iconify/react", + "moduleUrl": "https://github.com/iconify/iconify", + "moduleVersion": "6.0.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, + { + "moduleName": "@iconify/types", + "moduleUrl": "https://github.com/iconify/iconify", + "moduleVersion": "2.0.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "@isaacs/fs-minipass", "moduleUrl": "https://github.com/npm/fs-minipass", @@ -399,6 +420,20 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "@tanstack/react-virtual", + "moduleUrl": "https://github.com/TanStack/virtual", + "moduleVersion": "3.13.12", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, + { + "moduleName": "@tanstack/virtual-core", + "moduleUrl": "https://github.com/TanStack/virtual", + "moduleVersion": "3.13.12", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "@testing-library/dom", "moduleUrl": "https://github.com/testing-library/dom-testing-library", @@ -567,6 +602,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "bind-event-listener", + "moduleUrl": "https://github.com/alexreardon/bind-event-listener", + "moduleVersion": "3.0.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "brace-expansion", "moduleUrl": "https://github.com/juliangruber/brace-expansion", @@ -1246,13 +1288,6 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, - { - "moduleName": "material-symbols", - "moduleUrl": "https://github.com/marella/material-symbols", - "moduleVersion": "0.33.0", - "moduleLicense": "Apache-2.0", - "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" - }, { "moduleName": "math-intrinsics", "moduleUrl": "https://github.com/es-shims/math-intrinsics", @@ -1494,7 +1529,7 @@ { "moduleName": "postcss", "moduleUrl": "https://github.com/postcss/postcss", - "moduleVersion": "8.5.3", + "moduleVersion": "8.5.6", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -1526,6 +1561,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "raf-schd", + "moduleUrl": "https://github.com/alexreardon/raf-schd", + "moduleVersion": "4.0.3", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "react-dom", "moduleUrl": "https://github.com/facebook/react",