diff --git a/frontend/public/branding/StirlingPDFLogoNoTextDark.svg b/frontend/public/branding/StirlingPDFLogoNoTextDark.svg index 6c99f5001..001c6a01c 100644 --- a/frontend/public/branding/StirlingPDFLogoNoTextDark.svg +++ b/frontend/public/branding/StirlingPDFLogoNoTextDark.svg @@ -1,4 +1,4 @@ - - + + diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index ed3942172..976b4ae06 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -367,6 +367,10 @@ "title": "Add image", "desc": "Adds a image onto a set location on the PDF" }, + "attachments": { + "title": "Add Attachments", + "desc": "Add or remove embedded files (attachments) to/from a PDF" + }, "watermark": { "title": "Add Watermark", "desc": "Add a custom watermark to your PDF document." @@ -586,6 +590,34 @@ "replaceColorPdf": { "title": "Advanced Colour options", "desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size" + }, + "EMLToPDF": { + "title": "Email to PDF", + "desc": "Converts email (EML) files to PDF format including headers, body, and inline images" + }, + "fakeScan": { + "title": "Fake Scan", + "desc": "Create a PDF that looks like it was scanned" + }, + "editTableOfContents": { + "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." + }, + "read": { + "title": "Read", + "desc": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration." + }, + "reorganizePages": { + "title": "Reorganize Pages", + "desc": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control." } }, "viewPdf": { @@ -766,6 +798,15 @@ "upload": "Add image", "submit": "Add image" }, + "attachments": { + "tags": "attachments,add,remove,embed,file", + "title": "Add Attachments", + "header": "Add Attachments", + "add": "Add Attachment", + "remove": "Remove Attachment", + "embed": "Embed Attachment", + "submit": "Add Attachments" + }, "watermark": { "tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo", "title": "Add Watermark", @@ -1607,7 +1648,7 @@ "title": "How we use Cookies", "description": { "1": "We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love.", - "2": "If you’d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly." + "2": "If you'd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly." }, "acceptAllBtn": "Okay", "acceptNecessaryBtn": "No Thanks", @@ -1631,7 +1672,7 @@ "1": "Strictly Necessary Cookies", "2": "Always Enabled" }, - "description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off." + "description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can't be turned off." }, "analytics": { "title": "Analytics", diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 7aed3632b..53f0ba94e 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -25,31 +25,6 @@ function NavHeader({ const { handleReaderToggle, handleBackToTools } = useToolWorkflow(); return ( <> -
- - - - - - - - - - -
- {/* Divider after top icons */} - {/* All Tools button below divider */}
@@ -126,7 +101,10 @@ const QuickAccessBar = forwardRef(({ { id: 'automate', name: 'Automate', - icon: , + icon: + + automation + , tooltip: 'Automate workflows', size: 'lg', isRound: false, @@ -210,6 +188,9 @@ const QuickAccessBar = forwardRef(({ ref={ref} data-sidebar="quick-access" className={`h-screen flex flex-col w-20 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`} + style={{ + borderRight: '1px solid var(--border-default)' + }} > {/* Fixed header outside scrollable area */}
diff --git a/frontend/src/components/shared/RainbowThemeProvider.tsx b/frontend/src/components/shared/RainbowThemeProvider.tsx index 27cf5101f..21c46cf72 100644 --- a/frontend/src/components/shared/RainbowThemeProvider.tsx +++ b/frontend/src/components/shared/RainbowThemeProvider.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, ReactNode } from 'react'; -import { MantineProvider, ColorSchemeScript } from '@mantine/core'; +import { MantineProvider } from '@mantine/core'; import { useRainbowTheme } from '../../hooks/useRainbowTheme'; import { mantineTheme } from '../../theme/mantineTheme'; import rainbowStyles from '../../styles/rainbow.module.css'; @@ -34,22 +34,19 @@ export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) { const mantineColorScheme = rainbowTheme.themeMode === 'rainbow' ? 'dark' : rainbowTheme.themeMode; return ( - <> - - - + +
-
- {children} -
- - - + {children} +
+
+
); } diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index d392f21b6..675b06cdd 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -1,43 +1,282 @@ -import React from "react"; -import { Box, Text, Stack, Button } from "@mantine/core"; +import React, { useMemo, useRef, useLayoutEffect, useState } from "react"; +import { Box, Text, Stack } from "@mantine/core"; import { useTranslation } from "react-i18next"; -import { ToolRegistry } from "../../types/tool"; +import { baseToolRegistry, type ToolRegistryEntry } from "../../data/toolRegistry"; +import ToolButton from "./toolPicker/ToolButton"; +import "./toolPicker/ToolPicker.css"; interface ToolPickerProps { selectedToolKey: string | null; onSelect: (id: string) => void; - /** Pre-filtered tools to display */ - filteredTools: [string, ToolRegistry[string]][]; + filteredTools: [string, ToolRegistryEntry][]; +} + +interface GroupedTools { + [category: string]: { + [subcategory: string]: Array<{ id: string; tool: ToolRegistryEntry }>; + }; } const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProps) => { const { t } = useTranslation(); + const [quickHeaderHeight, setQuickHeaderHeight] = useState(0); + const [allHeaderHeight, setAllHeaderHeight] = useState(0); + + const scrollableRef = useRef(null); + const quickHeaderRef = useRef(null); + const allHeaderRef = useRef(null); + const quickAccessRef = useRef(null); + const allToolsRef = useRef(null); + + useLayoutEffect(() => { + const update = () => { + if (quickHeaderRef.current) { + setQuickHeaderHeight(quickHeaderRef.current.offsetHeight); + } + if (allHeaderRef.current) { + setAllHeaderHeight(allHeaderRef.current.offsetHeight); + } + }; + update(); + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); + }, []); + + const groupedTools = useMemo(() => { + const grouped: GroupedTools = {}; + filteredTools.forEach(([id, tool]) => { + const baseTool = baseToolRegistry[id as keyof typeof baseToolRegistry]; + const category = baseTool?.category || "OTHER"; + const subcategory = baseTool?.subcategory || "General"; + if (!grouped[category]) grouped[category] = {}; + if (!grouped[category][subcategory]) grouped[category][subcategory] = []; + grouped[category][subcategory].push({ id, tool }); + }); + return grouped; + }, [filteredTools]); + + const sections = useMemo(() => { + const mapping: Record = { + "RECOMMENDED TOOLS": "QUICK ACCESS", + "STANDARD TOOLS": "ALL TOOLS", + "ADVANCED TOOLS": "ALL TOOLS" + }; + const quick: Record> = {}; + const all: Record> = {}; + Object.entries(groupedTools).forEach(([origCat, subs]) => { + const bucket = mapping[origCat.toUpperCase()] || "ALL TOOLS"; + const target = bucket === "QUICK ACCESS" ? quick : all; + Object.entries(subs).forEach(([sub, tools]) => { + if (!target[sub]) target[sub] = []; + target[sub].push(...tools); + }); + }); + + const sortSubs = (obj: Record>) => + Object.entries(obj) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([subcategory, tools]) => ({ + subcategory, + tools: tools.sort((a, b) => a.tool.name.localeCompare(b.tool.name)) + })); + + return [ + { title: "QUICK ACCESS", ref: quickAccessRef, subcategories: sortSubs(quick) }, + { title: "ALL TOOLS", ref: allToolsRef, subcategories: sortSubs(all) } + ]; + }, [groupedTools]); + + const visibleSections = sections; + + const quickSection = useMemo( + () => visibleSections.find(s => s.title === "QUICK ACCESS"), + [visibleSections] + ); + const allSection = useMemo( + () => visibleSections.find(s => s.title === "ALL TOOLS"), + [visibleSections] + ); + + const scrollTo = (ref: React.RefObject) => { + const container = scrollableRef.current; + const target = ref.current; + if (container && target) { + const stackedOffset = ref === allToolsRef + ? (quickHeaderHeight + allHeaderHeight) + : quickHeaderHeight; + const top = target.offsetTop - container.offsetTop - (stackedOffset || 0); + container.scrollTo({ + top: Math.max(0, top), + behavior: "smooth" + }); + } + }; return ( - - - {filteredTools.length === 0 ? ( - + + + {quickSection && ( + <> +
scrollTo(quickAccessRef)} + > + QUICK ACCESS + + {quickSection.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)} + +
+ + + + {quickSection.subcategories.map(sc => ( + + {quickSection.subcategories.length > 1 && ( + + {sc.subcategory} + + )} + + {sc.tools.map(({ id, tool }) => ( + + ))} + + + ))} + + + + )} + + {allSection && ( + <> +
scrollTo(allToolsRef)} + > + ALL TOOLS + + {allSection.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)} + +
+ + + + {allSection.subcategories.map(sc => ( + + {allSection.subcategories.length > 1 && ( + + {sc.subcategory} + + )} + + {sc.tools.map(({ id, tool }) => ( + + ))} + + + ))} + + + + )} + + {!quickSection && !allSection && ( + {t("toolPicker.noToolsFound", "No tools found")} - ) : ( - filteredTools.map(([id, { icon, name }]) => ( - - )) )} -
+
); }; diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx new file mode 100644 index 000000000..06e3d5fd2 --- /dev/null +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Button, Tooltip } from "@mantine/core"; +import { type ToolRegistryEntry } from "../../../data/toolRegistry"; + +interface ToolButtonProps { + id: string; + tool: ToolRegistryEntry; + isSelected: boolean; + onSelect: (id: string) => void; +} + +const ToolButton: React.FC = ({ id, tool, isSelected, onSelect }) => { + return ( + + + + ); +}; + +export default ToolButton; \ No newline at end of file diff --git a/frontend/src/components/tools/toolPicker/ToolPicker.css b/frontend/src/components/tools/toolPicker/ToolPicker.css new file mode 100644 index 000000000..964e2f4ff --- /dev/null +++ b/frontend/src/components/tools/toolPicker/ToolPicker.css @@ -0,0 +1,52 @@ +.tool-picker-scrollable { + overflow-y: auto !important; + overflow-x: hidden !important; + scrollbar-width: thin; + scrollbar-color: var(--mantine-color-gray-4) transparent; +} + +.tool-picker-scrollable::-webkit-scrollbar { + width: 6px; +} + +.tool-picker-scrollable::-webkit-scrollbar-track { + background: transparent; +} + +.tool-picker-scrollable::-webkit-scrollbar-thumb { + background-color: var(--mantine-color-gray-4); + border-radius: 3px; +} + +.tool-picker-scrollable::-webkit-scrollbar-thumb:hover { + background-color: var(--mantine-color-gray-5); +} + +.search-input { + margin: 1rem; +} + +.tool-subcategory-title { + text-transform: uppercase; + padding-bottom: 0.5rem; + font-size: 0.75rem; + color: var(--tool-subcategory-text-color); + /* Align the text with tool labels to account for icon gutter */ + padding-left: 1rem; +} + +/* Compact tool buttons */ +.tool-button { + font-size: 0.875rem; /* default 1rem - 0.125rem? We'll apply exact -0.25rem via calc below */ + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + +.tool-button .mantine-Button-label { + font-size: .85rem; +} + +.tool-button-icon { + font-size: 1rem; + line-height: 1; +} \ No newline at end of file diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx new file mode 100644 index 000000000..901c0a054 --- /dev/null +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -0,0 +1,128 @@ +import React, { useState, useRef, useEffect, useMemo } from "react"; +import { TextInput, Stack, Button, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import SearchIcon from "@mui/icons-material/Search"; +import { type ToolRegistryEntry } from "../../../data/toolRegistry"; + +interface ToolSearchProps { + value: string; + onChange: (value: string) => void; + toolRegistry: Readonly>; + onToolSelect?: (toolId: string) => void; + mode: 'filter' | 'dropdown'; + selectedToolKey?: string | null; +} + +const ToolSearch = ({ + value, + onChange, + toolRegistry, + onToolSelect, + mode = 'filter', + selectedToolKey +}: ToolSearchProps) => { + const { t } = useTranslation(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const searchRef = useRef(null); + + const filteredTools = useMemo(() => { + if (!value.trim()) return []; + return Object.entries(toolRegistry) + .filter(([id, tool]) => { + if (mode === 'dropdown' && id === selectedToolKey) return false; + return tool.name.toLowerCase().includes(value.toLowerCase()) || + tool.description.toLowerCase().includes(value.toLowerCase()); + }) + .slice(0, 6) + .map(([id, tool]) => ({ id, tool })); + }, [value, toolRegistry, mode, selectedToolKey]); + + const handleSearchChange = (searchValue: string) => { + onChange(searchValue); + if (mode === 'dropdown') { + setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0); + } + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (searchRef.current && !searchRef.current.contains(event.target as Node)) { + setDropdownOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const searchInput = ( + handleSearchChange(e.currentTarget.value)} + autoComplete="off" + className="search-input rounded-lg" + leftSection={} + /> + ); + + if (mode === 'filter') { + return searchInput; + } + + return ( +
+ {searchInput} + {dropdownOpen && filteredTools.length > 0 && ( +
+ + {filteredTools.map(({ id, tool }) => ( + + ))} + +
+ )} +
+ ); +}; + +export default ToolSearch; \ No newline at end of file diff --git a/frontend/src/data/toolRegistry.tsx b/frontend/src/data/toolRegistry.tsx new file mode 100644 index 000000000..7e09f7f1d --- /dev/null +++ b/frontend/src/data/toolRegistry.tsx @@ -0,0 +1,529 @@ +import React from 'react'; +import SplitPdfPanel from "../tools/Split"; +import CompressPdfPanel from "../tools/Compress"; +import MergePdfPanel from "../tools/Merge"; +import OCRPanel from '../tools/OCR'; +import ConvertPanel from '../tools/Convert'; + +export type ToolRegistryEntry = { + icon: React.ReactNode; + name: string; + component: React.ComponentType | null; + view: string; + description: string; + category: string; + subcategory: string | null; +}; + +export type ToolRegistry = { + [key: string]: ToolRegistryEntry; +}; + +export const baseToolRegistry: ToolRegistry = { + "add-attachments": { + icon: attachment, + name: "home.attachments.title", + component: null, + view: "format", + description: "home.attachments.desc", + category: "Standard Tools", + subcategory: "Page Formatting", + }, + "add-image": { + icon: image, + name: "home.addImage.title", + component: null, + view: "format", + description: "home.addImage.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "add-page-numbers": { + icon: 123, + name: "home.add-page-numbers.title", + component: null, + view: "format", + description: "home.add-page-numbers.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "add-password": { + icon: password, + name: "home.addPassword.title", + component: null, + view: "security", + description: "home.addPassword.desc", + category: "Standard Tools", + subcategory: "Document Security" + }, + "add-stamp": { + icon: approval, + name: "home.AddStampRequest.title", + component: null, + view: "format", + description: "home.AddStampRequest.desc", + category: "Standard Tools", + subcategory: "Document Security" + }, + "add-watermark": { + icon: branding_watermark, + name: "home.watermark.title", + component: null, + view: "format", + description: "home.watermark.desc", + category: "Standard Tools", + subcategory: "Document Security" + }, + "adjust-colors-contrast": { + icon: palette, + name: "home.adjust-contrast.title", + component: null, + view: "format", + description: "home.adjust-contrast.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "adjust-page-size-scale": { + icon: crop_free, + name: "home.scalePages.title", + component: null, + view: "format", + description: "home.scalePages.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "auto-rename-pdf-file": { + icon: match_word, + name: "home.auto-rename.title", + component: null, + view: "format", + description: "home.auto-rename.desc", + category: "Advanced Tools", + subcategory: "Automation" + }, + "auto-split-by-size-count": { + icon: content_cut, + name: "home.autoSizeSplitPDF.title", + component: null, + view: "format", + description: "home.autoSizeSplitPDF.desc", + category: "Advanced Tools", + subcategory: "Automation" + }, + "auto-split-pages": { + icon: split_scene_right, + name: "home.autoSplitPDF.title", + component: null, + view: "format", + description: "home.autoSplitPDF.desc", + category: "Advanced Tools", + subcategory: "Automation" + }, + "automate": { + icon: automation, + name: "home.automate.title", + component: null, + view: "format", + description: "home.automate.desc", + category: "Advanced Tools", + subcategory: "Automation" + }, + "certSign": { + icon: workspace_premium, + name: "home.certSign.title", + component: null, + view: "sign", + description: "home.certSign.desc", + category: "Standard Tools", + subcategory: "Signing" + }, + "change-metadata": { + icon: assignment, + name: "home.changeMetadata.title", + component: null, + view: "format", + description: "home.changeMetadata.desc", + category: "Standard Tools", + subcategory: "Document Review" + }, + "change-permissions": { + icon: admin_panel_settings, + name: "home.permissions.title", + component: null, + view: "security", + description: "home.permissions.desc", + category: "Standard Tools", + subcategory: "Document Review" + }, + "compare": { + icon: compare, + name: "home.compare.title", + component: null, + view: "format", + description: "home.compare.desc", + category: "Recommended Tools", + subcategory: null + }, + "compressPdfs": { + icon: zoom_in_map, + name: "home.compressPdfs.title", + component: CompressPdfPanel, + view: "compress", + description: "home.compressPdfs.desc", + category: "Recommended Tools", + subcategory: null + }, + "convert": { + icon: sync_alt, + name: "home.fileToPDF.title", + component: ConvertPanel, + view: "convert", + description: "home.fileToPDF.desc", + category: "Recommended Tools", + subcategory: null + }, + "cropPdf": { + icon: crop, + name: "home.crop.title", + component: null, + view: "format", + description: "home.crop.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "detect-split-scanned-photos": { + icon: scanner, + name: "home.ScannerImageSplit.title", + component: null, + view: "format", + description: "home.ScannerImageSplit.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "edit-table-of-contents": { + icon: bookmark_add, + name: "home.editTableOfContents.title", + component: null, + view: "format", + description: "home.editTableOfContents.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "extract-images": { + icon: filter, + name: "home.extractImages.title", + component: null, + view: "extract", + description: "home.extractImages.desc", + category: "Standard Tools", + subcategory: "Extraction" + }, + "extract-pages": { + icon: upload, + name: "home.extractPage.title", + component: null, + view: "extract", + description: "home.extractPage.desc", + category: "Standard Tools", + subcategory: "Extraction" + }, + "flatten": { + icon: layers_clear, + name: "home.flatten.title", + component: null, + view: "format", + description: "home.flatten.desc", + category: "Standard Tools", + subcategory: "Document Security" + }, + "get-all-info-on-pdf": { + icon: fact_check, + name: "home.getPdfInfo.title", + component: null, + view: "extract", + description: "home.getPdfInfo.desc", + category: "Standard Tools", + subcategory: "Verification" + }, + "manage-certificates": { + icon: license, + name: "home.manageCertificates.title", + component: null, + view: "security", + description: "home.manageCertificates.desc", + category: "Standard Tools", + subcategory: "Document Security" + }, + "mergePdfs": { + icon: library_add, + name: "home.merge.title", + component: MergePdfPanel, + view: "merge", + description: "home.merge.desc", + category: "Recommended Tools", + subcategory: null + }, + "multi-page-layout": { + icon: dashboard, + name: "home.pageLayout.title", + component: null, + view: "format", + description: "home.pageLayout.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "multi-tool": { + icon: dashboard_customize, + name: "home.multiTool.title", + component: null, + view: "pageEditor", + description: "home.multiTool.desc", + category: "Recommended Tools", + subcategory: null + }, + "ocr": { + icon: quick_reference_all, + name: "home.ocr.title", + component: OCRPanel, + view: "convert", + description: "home.ocr.desc", + category: "Recommended Tools", + subcategory: null + }, + "overlay-pdfs": { + icon: layers, + name: "home.overlay-pdfs.title", + component: null, + view: "format", + description: "home.overlay-pdfs.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "read": { + icon: article, + name: "home.read.title", + component: null, + view: "view", + description: "home.read.desc", + category: "Standard Tools", + subcategory: "Document Review" + }, + "redact": { + icon: visibility_off, + name: "home.redact.title", + component: null, + view: "redact", + description: "home.redact.desc", + category: "Recommended Tools", + subcategory: null + }, + "remove": { + icon: delete, + name: "home.removePages.title", + component: null, + view: "remove", + description: "home.removePages.desc", + category: "Standard Tools", + subcategory: "Removal" + }, + "remove-annotations": { + icon: thread_unread, + name: "home.removeAnnotations.title", + component: null, + view: "remove", + description: "home.removeAnnotations.desc", + category: "Standard Tools", + subcategory: "Removal" + }, + "remove-blank-pages": { + icon: scan_delete, + name: "home.removeBlanks.title", + component: null, + view: "remove", + description: "home.removeBlanks.desc", + category: "Standard Tools", + subcategory: "Removal" + }, + "remove-certificate-sign": { + icon: remove_moderator, + name: "home.removeCertSign.title", + component: null, + view: "security", + description: "home.removeCertSign.desc", + category: "Standard Tools", + subcategory: "Removal" + }, + "remove-image": { + icon: remove_selection, + name: "home.removeImagePdf.title", + component: null, + view: "format", + description: "home.removeImagePdf.desc", + category: "Standard Tools", + subcategory: "Removal" + }, + "remove-password": { + icon: lock_open_right, + name: "home.removePassword.title", + component: null, + view: "security", + description: "home.removePassword.desc", + category: "Standard Tools", + subcategory: "Removal" + }, + "repair": { + icon: build, + name: "home.repair.title", + component: null, + view: "format", + description: "home.repair.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "replace-and-invert-color": { + icon: format_color_fill, + name: "home.replaceColorPdf.title", + component: null, + view: "format", + description: "home.replaceColorPdf.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "reorganize-pages": { + icon: move_down, + name: "home.reorganizePages.title", + component: null, + view: "pageEditor", + description: "home.reorganizePages.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "rotate": { + icon: rotate_right, + name: "home.rotate.title", + component: null, + view: "format", + description: "home.rotate.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "sanitize": { + icon: sanitizer, + name: "home.sanitizePdf.title", + component: null, + view: "security", + description: "home.sanitizePdf.desc", + category: "Standard Tools", + subcategory: "Document Security" + }, + "scanner-effect": { + icon: scanner, + name: "home.fakeScan.title", + component: null, + view: "format", + description: "home.fakeScan.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "show-javascript": { + icon: javascript, + name: "home.showJS.title", + component: null, + view: "extract", + description: "home.showJS.desc", + category: "Advanced Tools", + subcategory: "Developer Tools" + }, + "sign": { + icon: signature, + name: "home.sign.title", + component: null, + view: "sign", + description: "home.sign.desc", + category: "Standard Tools", + subcategory: "Signing" + }, + "single-large-page": { + icon: looks_one, + name: "home.PdfToSinglePage.title", + component: null, + view: "format", + description: "home.PdfToSinglePage.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "split": { + icon: content_cut, + name: "home.split.title", + component: null, + view: "format", + description: "home.split.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "split-by-chapters": { + icon: collections_bookmark, + name: "home.splitPdfByChapters.title", + component: null, + view: "format", + description: "home.splitPdfByChapters.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "split-by-sections": { + icon: grid_on, + name: "home.split-by-sections.title", + component: null, + view: "format", + description: "home.split-by-sections.desc", + category: "Advanced Tools", + subcategory: "Advanced Formatting" + }, + "splitPdf": { + icon: content_cut, + name: "home.split.title", + component: SplitPdfPanel, + view: "split", + description: "home.split.desc", + category: "Standard Tools", + subcategory: "Page Formatting" + }, + "unlock-pdf-forms": { + icon: preview_off, + name: "home.unlockPDFForms.title", + component: null, + view: "security", + description: "home.unlockPDFForms.desc", + category: "Standard Tools", + subcategory: "Document Security" + }, + "validate-pdf-signature": { + icon: verified, + name: "home.validateSignature.title", + component: null, + view: "security", + description: "home.validateSignature.desc", + category: "Standard Tools", + subcategory: "Verification" + }, + "view-pdf": { + icon: article, + name: "home.viewPdf.title", + component: null, + view: "view", + description: "home.viewPdf.desc", + category: "Recommended Tools", + subcategory: null + } +}; + +export const toolEndpoints: Record = { + split: ["split-pages", + "split-pdf-by-sections", + "split-by-size-or-count", + "split-pdf-by-chapters"], + compressPdfs: ["compress-pdf"], + merge: ["merge-pdfs"], + // Add more endpoint mappings as needed +}; \ No newline at end of file diff --git a/frontend/src/hooks/useRainbowTheme.ts b/frontend/src/hooks/useRainbowTheme.ts index a3c8f6e67..b16ed1228 100644 --- a/frontend/src/hooks/useRainbowTheme.ts +++ b/frontend/src/hooks/useRainbowTheme.ts @@ -18,7 +18,13 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb if (stored && ['light', 'dark', 'rainbow'].includes(stored)) { return stored as ThemeMode; } - return initialTheme; + try { + // Fallback to OS preference if available + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return prefersDark ? 'dark' : initialTheme; + } catch { + return initialTheme; + } }); // Track rapid toggles for easter egg diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index debd3f5b1..cabfa0db9 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -1,88 +1,13 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import ContentCutIcon from "@mui/icons-material/ContentCut"; -import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; -import SwapHorizIcon from "@mui/icons-material/SwapHoriz"; -import ApiIcon from "@mui/icons-material/Api"; +import { baseToolRegistry, toolEndpoints, type ToolRegistryEntry } from "../data/toolRegistry"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; -import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool"; - - -// Add entry here with maxFiles, endpoints, and lazy component -const toolDefinitions: Record = { - split: { - id: "split", - icon: , - component: React.lazy(() => import("../tools/Split")), - maxFiles: 1, - category: "manipulation", - description: "Split PDF files into smaller parts", - endpoints: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"] - }, - compress: { - id: "compress", - icon: , - component: React.lazy(() => import("../tools/Compress")), - maxFiles: -1, - category: "optimization", - description: "Reduce PDF file size", - endpoints: ["compress-pdf"] - }, - convert: { - id: "convert", - icon: , - component: React.lazy(() => import("../tools/Convert")), - maxFiles: -1, - category: "manipulation", - description: "Change to and from PDF and other formats", - endpoints: ["pdf-to-img", "img-to-pdf", "pdf-to-word", "pdf-to-presentation", "pdf-to-text", "pdf-to-html", "pdf-to-xml", "html-to-pdf", "markdown-to-pdf", "file-to-pdf"], - supportedFormats: [ - // Microsoft Office - "doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx", - // OpenDocument - "odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg", - // Text formats - "txt", "text", "xml", "rtf", "html", "lwp", "md", - // Images - "bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp", - // StarOffice - "sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw", - // Email formats - "eml", - // Archive formats - "zip", - // Other - "dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf" - ] - }, - swagger: { - id: "swagger", - icon: , - component: React.lazy(() => import("../tools/SwaggerUI")), - maxFiles: 0, - category: "utility", - description: "Open API documentation", - endpoints: ["swagger-ui"] - }, - ocr: { - id: "ocr", - icon: - quick_reference_all - , - component: React.lazy(() => import("../tools/OCR")), - maxFiles: -1, - category: "utility", - description: "Extract text from images using OCR", - endpoints: ["ocr-pdf"] - }, - -}; interface ToolManagementResult { selectedToolKey: string | null; - selectedTool: Tool | null; + selectedTool: ToolRegistryEntry | null; toolSelectedFileIds: string[]; - toolRegistry: ToolRegistry; + toolRegistry: Record; selectTool: (toolKey: string) => void; clearToolSelection: () => void; setToolSelectedFileIds: (fileIds: string[]) => void; @@ -95,30 +20,30 @@ export const useToolManagement = (): ToolManagementResult => { const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]); const allEndpoints = Array.from(new Set( - Object.values(toolDefinitions).flatMap(tool => tool.endpoints || []) + Object.values(toolEndpoints).flat() as string[] )); const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints); const isToolAvailable = useCallback((toolKey: string): boolean => { if (endpointsLoading) return true; - const tool = toolDefinitions[toolKey]; - if (!tool?.endpoints) return true; - return tool.endpoints.some(endpoint => endpointStatus[endpoint] === true); + const endpoints = toolEndpoints[toolKey] || []; + return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true); }, [endpointsLoading, endpointStatus]); - const toolRegistry: ToolRegistry = useMemo(() => { - const availableTools: ToolRegistry = {}; - Object.keys(toolDefinitions).forEach(toolKey => { + const toolRegistry: Record = useMemo(() => { + const availableToolRegistry: Record = {}; + Object.keys(baseToolRegistry).forEach(toolKey => { if (isToolAvailable(toolKey)) { - const toolDef = toolDefinitions[toolKey]; - availableTools[toolKey] = { - ...toolDef, - name: t(`home.${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1)) + const baseTool = baseToolRegistry[toolKey as keyof typeof baseToolRegistry]; + availableToolRegistry[toolKey] = { + ...baseTool, + name: t(baseTool.name), + description: t(baseTool.description) }; } }); - return availableTools; - }, [t, isToolAvailable]); + return availableToolRegistry; + }, [isToolAvailable, t]); useEffect(() => { if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index e8d719e24..740eec3dc 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -2,11 +2,22 @@ import '@mantine/core/styles.css'; import './index.css'; // Import Tailwind CSS import React from 'react'; import ReactDOM from 'react-dom/client'; -import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/core'; +import { ColorSchemeScript } from '@mantine/core'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import './i18n'; // Initialize i18next +// Compute initial color scheme +function getInitialScheme(): 'light' | 'dark' { + const stored = localStorage.getItem('stirling-theme'); + if (stored === 'light' || stored === 'dark') return stored; + try { + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return prefersDark ? 'dark' : 'light'; + } catch { + return 'light'; + } +} const container = document.getElementById('root'); if (!container) { @@ -15,12 +26,10 @@ if (!container) { const root = ReactDOM.createRoot(container); // Finds the root DOM element root.render( - - - - - - + + + + ); diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 783a10e9d..46ca33bef 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -60,4 +60,4 @@ export default function HomePage() { ); -} +} \ No newline at end of file diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css index 9ec48bca7..b23707efb 100644 --- a/frontend/src/styles/theme.css +++ b/frontend/src/styles/theme.css @@ -81,7 +81,7 @@ --text-secondary: #4b5563; --text-muted: #6b7280; --border-subtle: #e5e7eb; - --border-default: #d1d5db; + --border-default: #E2E8F0; --border-strong: #9ca3af; --hover-bg: #f9fafb; --active-bg: #f3f4f6; @@ -117,6 +117,18 @@ --icon-inactive-bg: #9CA3AF; --icon-inactive-color: #FFFFFF; + /* New theme colors for text and icons */ + --tools-text-and-icon-color: #374151; + + /* Tool picker sticky header variables (light mode) */ + --tool-header-bg: #DBEFFF; + --tool-header-border: #BEE2FF; + --tool-header-text: #1E88E5; + --tool-header-badge-bg: #C0DDFF; + --tool-header-badge-text: #004E99; + + /* Subcategory title styling (light mode) */ + --tool-subcategory-text-color: #6B7280; --accent-interactive: #4A90E2; --text-instruction: #4A90E2; --text-brand: var(--color-gray-700); @@ -177,7 +189,7 @@ --bg-raised: #1F2329; --bg-muted: #1F2329; --bg-background: #2A2F36; - --bg-toolbar: #272A2E; + --bg-toolbar: #1F2329; --bg-file-manager: #1F2329; --bg-file-list: #2A2F36; --btn-open-file: #0A8BFF; @@ -185,7 +197,7 @@ --text-secondary: #d1d5db; --text-muted: #9ca3af; --border-subtle: #2A2F36; - --border-default: #374151; + --border-default: #3A4047; --border-strong: #4b5563; --hover-bg: #374151; --active-bg: #4b5563; @@ -251,6 +263,18 @@ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4); --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4); + + --tools-text-and-icon-color: #D0D6DC; + + /* Tool picker sticky header variables (dark mode) */ + --tool-header-bg: #2A2F36; + --tool-header-border: #3A4047; + --tool-header-text: #D0D6DC; + --tool-header-badge-bg: #4B525A; + --tool-header-badge-text: #FFFFFF; + + /* Subcategory title styling (dark mode) */ + --tool-subcategory-text-color: #6B7280; } /* Dropzone drop state styling */