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 1/6] 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 2/6] 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 + ) : ( )} 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 3/6] 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", From bbd658d3b83fa20a2d14ca9ce5e4b9e93d4319d4 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:25:33 +0100 Subject: [PATCH 4/6] Default View to file editor not page editor (#4258) Co-authored-by: Connor Yoh --- frontend/src/types/navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/types/navigation.ts b/frontend/src/types/navigation.ts index 61aa24cc3..70d108c9a 100644 --- a/frontend/src/types/navigation.ts +++ b/frontend/src/types/navigation.ts @@ -33,7 +33,7 @@ export const isValidMode = (mode: string): mode is ModeType => { return validModes.includes(mode as ModeType); }; -export const getDefaultMode = (): ModeType => 'pageEditor'; +export const getDefaultMode = (): ModeType => 'fileEditor'; // Route parsing result export interface ToolRoute { From 42d7664e25044cefa8a96d4cb2ccea214e5d5ff9 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:26:26 +0100 Subject: [PATCH 5/6] Preview file (#4260) Preview file works Co-authored-by: Connor Yoh --- frontend/src/contexts/ToolWorkflowContext.tsx | 5 ++++- frontend/src/tools/Automate.tsx | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index f784dd49a..2bbb3c9f4 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -134,7 +134,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const setPreviewFile = useCallback((file: File | null) => { dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); - }, []); + if (file) { + actions.setMode('viewer'); + } + }, [actions]); const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => { dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions }); diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index af6b3d411..7e75f0cbf 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useFileContext } from "../contexts/FileContext"; import { useFileSelection } from "../contexts/FileContext"; +import { useNavigation } from "../contexts/NavigationContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { createFilesToolStep } from "../components/tools/shared/FilesToolStep"; @@ -19,6 +20,7 @@ import { AUTOMATION_STEPS } from "../constants/automation"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { selectedFiles } = useFileSelection(); + const { setMode } = useNavigation(); const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(AUTOMATION_STEPS.SELECTION); const [stepData, setStepData] = useState({ step: AUTOMATION_STEPS.SELECTION }); @@ -171,7 +173,11 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { review: { isVisible: hasResults && currentStep === AUTOMATION_STEPS.RUN, operation: automateOperation, - title: t('automate.reviewTitle', 'Automation Results') + title: t('automate.reviewTitle', 'Automation Results'), + onFileClick: (file: File) => { + onPreviewFile?.(file); + setMode('viewer'); + } } }); }; From fe9d2367d5e086f68453b5e09e72f55c812be551 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:44:30 +0100 Subject: [PATCH 6/6] AutomateFixes (#4281) can edit automations drop down styles drop down bug fixes --------- Co-authored-by: Connor Yoh --- frontend/src/components/shared/TextInput.tsx | 6 +- .../tools/automate/AutomationCreation.tsx | 27 ++- .../components/tools/automate/ToolList.tsx | 116 +++++++----- .../tools/automate/ToolSelector.tsx | 168 ++++++++++-------- .../tools/toolPicker/ToolButton.tsx | 15 +- .../tools/toolPicker/ToolPicker.css | 2 +- .../tools/toolPicker/ToolSearch.tsx | 78 ++++---- .../hooks/tools/automate/useAutomationForm.ts | 12 +- frontend/src/tools/Automate.tsx | 10 +- frontend/src/types/automation.ts | 4 +- 10 files changed, 264 insertions(+), 174 deletions(-) diff --git a/frontend/src/components/shared/TextInput.tsx b/frontend/src/components/shared/TextInput.tsx index fc3e99015..a5dd90569 100644 --- a/frontend/src/components/shared/TextInput.tsx +++ b/frontend/src/components/shared/TextInput.tsx @@ -31,6 +31,8 @@ export interface TextInputProps { readOnly?: boolean; /** Accessibility label */ 'aria-label'?: string; + /** Focus event handler */ + onFocus?: () => void; } export const TextInput = forwardRef(({ @@ -46,6 +48,7 @@ export const TextInput = forwardRef(({ disabled = false, readOnly = false, 'aria-label': ariaLabel, + onFocus, ...props }, ref) => { const { colorScheme } = useMantineColorScheme(); @@ -63,7 +66,7 @@ export const TextInput = forwardRef(({ return (
{icon && ( - @@ -81,6 +84,7 @@ export const TextInput = forwardRef(({ disabled={disabled} readOnly={readOnly} aria-label={ariaLabel} + onFocus={onFocus} style={{ backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF', color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382', diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index 49b12c396..ee301ae8e 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -98,7 +98,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o const saveAutomation = async () => { if (!canSaveAutomation()) return; - const automation = { + const automationData = { name: automationName.trim(), description: '', operations: selectedTools.map(tool => ({ @@ -109,7 +109,30 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o try { const { automationStorage } = await import('../../../services/automationStorage'); - const savedAutomation = await automationStorage.saveAutomation(automation); + let savedAutomation; + + if (mode === AutomationMode.EDIT && existingAutomation) { + // For edit mode, check if name has changed + const nameChanged = automationName.trim() !== existingAutomation.name; + + if (nameChanged) { + // Name changed - create new automation + savedAutomation = await automationStorage.saveAutomation(automationData); + } else { + // Name unchanged - update existing automation + const updatedAutomation = { + ...existingAutomation, + ...automationData, + id: existingAutomation.id, + createdAt: existingAutomation.createdAt + }; + savedAutomation = await automationStorage.updateAutomation(updatedAutomation); + } + } else { + // Create mode - always create new automation + savedAutomation = await automationStorage.saveAutomation(automationData); + } + onComplete(savedAutomation); } catch (error) { console.error('Error saving automation:', error); diff --git a/frontend/src/components/tools/automate/ToolList.tsx b/frontend/src/components/tools/automate/ToolList.tsx index 8b24b5c17..b11140ac5 100644 --- a/frontend/src/components/tools/automate/ToolList.tsx +++ b/frontend/src/components/tools/automate/ToolList.tsx @@ -1,14 +1,13 @@ -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'; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Text, Stack, Group, ActionIcon } from "@mantine/core"; +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[]; @@ -29,35 +28,39 @@ export default function ToolList({ onToolConfigure, onToolAdd, getToolName, - getToolDefaultParameters + 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 + parameters: defaultParams, }); }; return (
- - {t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length}) + + {t("automate.creation.tools.selected", "Selected Tools")} ({tools.length}) {tools.map((tool, index) => (
{/* Delete X in top right */} @@ -65,26 +68,26 @@ export default function ToolList({ variant="subtle" size="xs" onClick={() => onToolRemove(index)} - title={t('automate.creation.tools.remove', 'Remove tool')} + title={t("automate.creation.tools.remove", "Remove tool")} style={{ - position: 'absolute', - top: '4px', - right: '4px', + position: "absolute", + top: "4px", + right: "4px", zIndex: 1, - color: 'var(--mantine-color-gray-6)' + color: "var(--mantine-color-gray-6)", }} > - + -
+
{/* Tool Selection Dropdown with inline settings cog */}
handleToolSelect(index, newOperation)} - excludeTools={['automate']} + excludeTools={["automate"]} toolRegistry={toolRegistry} selectedValue={tool.operation} placeholder={tool.name} @@ -97,26 +100,37 @@ export default function ToolList({ variant="subtle" size="sm" onClick={() => onToolConfigure(index)} - title={t('automate.creation.tools.configure', 'Configure tool')} - style={{ color: 'var(--mantine-color-gray-6)' }} + 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")} - - )}
- + {/* Configuration status underneath */} + {tool.operation && !tool.configured && ( +
+ + {t("automate.creation.tools.notConfigured", "! Not Configured")} + +
+ )} {index < tools.length - 1 && ( -
- +
+ + ↓ +
)} @@ -124,19 +138,23 @@ export default function ToolList({ {/* 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 index 80b68b0a4..4fb87548f 100644 --- a/frontend/src/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -1,10 +1,11 @@ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Menu, Stack, Text, ScrollArea } from '@mantine/core'; +import { 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'; +import ToolButton from '../toolPicker/ToolButton'; interface ToolSelectorProps { onSelect: (toolKey: string) => void; @@ -24,6 +25,8 @@ export default function ToolSelector({ const { t } = useTranslation(); const [opened, setOpened] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + const [shouldAutoFocus, setShouldAutoFocus] = useState(false); + const containerRef = useRef(null); // Filter out excluded tools (like 'automate' itself) const baseFilteredTools = useMemo(() => { @@ -66,13 +69,21 @@ export default function ToolSelector({ } if (!sections || sections.length === 0) { + // If no sections, create a simple group from filtered tools + if (baseFilteredTools.length > 0) { + return [{ + name: 'All Tools', + subcategoryId: 'all' as any, + tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool })) + }]; + } 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]); + }, [isSearching, searchGroups, sections, baseFilteredTools]); const handleToolSelect = useCallback((toolKey: string) => { onSelect(toolKey); @@ -88,8 +99,25 @@ export default function ToolSelector({ const handleSearchFocus = () => { setOpened(true); + setShouldAutoFocus(true); // Request auto-focus for the input }; + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setOpened(false); + setSearchTerm(''); + } + }; + + if (opened) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [opened]); + + const handleSearchChange = (value: string) => { setSearchTerm(value); if (!opened) { @@ -97,6 +125,14 @@ export default function ToolSelector({ } }; + const handleInputFocus = () => { + if (!opened) { + setOpened(true); + } + // Clear auto-focus flag since input is now focused + setShouldAutoFocus(false); + }; + // Get display value for selected tool const getDisplayValue = () => { if (selectedValue && toolRegistry[selectedValue]) { @@ -106,77 +142,63 @@ export default function ToolSelector({ }; 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 - - )} -
-
+
+ {/* Always show the target - either selected tool or search input */} - - - - {displayGroups.length === 0 ? ( - - {isSearching - ? t('tools.noSearchResults', 'No tools found') - : t('tools.noTools', 'No tools available') - } - - ) : ( - renderedTools - )} - - - -
+ {selectedValue && toolRegistry[selectedValue] && !opened ? ( + // Show selected tool in AutomationEntry style when tool is selected and dropdown closed +
+ {}} rounded={true}> +
+ ) : ( + // Show search input when no tool selected OR when dropdown is opened + + )} + + {/* Custom dropdown */} + {opened && ( +
+ + + {displayGroups.length === 0 ? ( + + {isSearching + ? t('tools.noSearchResults', 'No tools found') + : t('tools.noTools', 'No tools available') + } + + ) : ( + renderedTools + )} + + +
+ )}
); } diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx index 66bd9489e..185eed5ed 100644 --- a/frontend/src/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -9,9 +9,10 @@ interface ToolButtonProps { tool: ToolRegistryEntry; isSelected: boolean; onSelect: (id: string) => void; + rounded?: boolean; } -const ToolButton: React.FC = ({ id, tool, isSelected, onSelect }) => { +const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, rounded = false }) => { const handleClick = (id: string) => { if (tool.link) { // Open external link in new tab @@ -33,7 +34,17 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect fullWidth justify="flex-start" className="tool-button" - styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)" } }} + styles={{ + root: { + borderRadius: rounded ? 'var(--mantine-radius-lg)' : 0, + color: "var(--tools-text-and-icon-color)", + ...(rounded && { + '&:hover': { + borderRadius: 'var(--mantine-radius-lg)', + } + }) + } + }} > void; toolRegistry: Readonly>; onToolSelect?: (toolId: string) => void; - mode: 'filter' | 'dropdown'; + mode: "filter" | "dropdown" | "unstyled"; selectedToolKey?: string | null; placeholder?: string; hideIcon?: boolean; onFocus?: () => void; + autoFocus?: boolean; } const ToolSearch = ({ @@ -23,11 +24,12 @@ const ToolSearch = ({ onChange, toolRegistry, onToolSelect, - mode = 'filter', + mode = "filter", selectedToolKey, placeholder, hideIcon = false, - onFocus + onFocus, + autoFocus = false, }: ToolSearchProps) => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -38,9 +40,10 @@ const ToolSearch = ({ 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()); + 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 })); @@ -48,7 +51,7 @@ const ToolSearch = ({ const handleSearchChange = (searchValue: string) => { onChange(searchValue); - if (mode === 'dropdown') { + if (mode === "dropdown") { setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0); } }; @@ -64,12 +67,20 @@ const ToolSearch = ({ setDropdownOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); }, []); + // Auto-focus the input when requested + useEffect(() => { + if (autoFocus && searchRef.current) { + setTimeout(() => { + searchRef.current?.focus(); + }, 10); + } + }, [autoFocus]); + const searchInput = ( -
} autoComplete="off" - + onFocus={onFocus} /> -
); - if (mode === 'filter') { + if (mode === "filter") { + return
{searchInput}
; + } + + if (mode === "unstyled") { return searchInput; } return ( -
+
{searchInput} {dropdownOpen && filteredTools.length > 0 && (
- + {filteredTools.map(({ id, tool }) => (
- } + leftSection={
{tool.icon}
} fullWidth justify="flex-start" style={{ - borderRadius: '6px', - color: 'var(--tools-text-and-icon-color)', - padding: '8px 12px' + borderRadius: "6px", + color: "var(--tools-text-and-icon-color)", + padding: "8px 12px", }} > -
+
{tool.name}
- + {tool.description}
diff --git a/frontend/src/hooks/tools/automate/useAutomationForm.ts b/frontend/src/hooks/tools/automate/useAutomationForm.ts index 11464a329..7bbe14d9b 100644 --- a/frontend/src/hooks/tools/automate/useAutomationForm.ts +++ b/frontend/src/hooks/tools/automate/useAutomationForm.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation'; import { AUTOMATION_CONSTANTS } from '../../../constants/automation'; @@ -16,18 +16,18 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us const [automationName, setAutomationName] = useState(''); const [selectedTools, setSelectedTools] = useState([]); - const getToolName = (operation: string) => { + const getToolName = useCallback((operation: string) => { const tool = toolRegistry?.[operation] as any; return tool?.name || t(`tools.${operation}.name`, operation); - }; + }, [toolRegistry, t]); - const getToolDefaultParameters = (operation: string): Record => { + const getToolDefaultParameters = useCallback((operation: string): Record => { const config = toolRegistry[operation]?.operationConfig; if (config?.defaultParameters) { return { ...config.defaultParameters }; } return {}; - }; + }, [toolRegistry]); // Initialize based on mode and existing automation useEffect(() => { @@ -58,7 +58,7 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us })); setSelectedTools(defaultTools); } - }, [mode, existingAutomation, selectedTools.length, t, getToolName]); + }, [mode, existingAutomation, t, getToolName]); const addTool = (operation: string) => { const newTool: AutomationTool = { diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 7e75f0cbf..444cdfce9 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -14,7 +14,7 @@ import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperati import { BaseToolProps } from "../types/tool"; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations"; -import { AutomationConfig, AutomationStepData, AutomationMode } from "../types/automation"; +import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation"; import { AUTOMATION_STEPS } from "../constants/automation"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { @@ -22,7 +22,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { selectedFiles } = useFileSelection(); const { setMode } = useNavigation(); - const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(AUTOMATION_STEPS.SELECTION); + const [currentStep, setCurrentStep] = useState(AUTOMATION_STEPS.SELECTION); const [stepData, setStepData] = useState({ step: AUTOMATION_STEPS.SELECTION }); const automateOperation = useAutomateOperation(); @@ -64,7 +64,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const renderCurrentStep = () => { switch (currentStep) { - case 'selection': + case AUTOMATION_STEPS.SELECTION: return ( { /> ); - case 'creation': + case AUTOMATION_STEPS.CREATION: if (!stepData.mode) { console.error('Creation mode is undefined'); return null; @@ -100,7 +100,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { /> ); - case 'run': + case AUTOMATION_STEPS.RUN: if (!stepData.automation) { console.error('Automation config is undefined'); return null; diff --git a/frontend/src/types/automation.ts b/frontend/src/types/automation.ts index ffbfe36b2..8d2cb5ae8 100644 --- a/frontend/src/types/automation.ts +++ b/frontend/src/types/automation.ts @@ -24,8 +24,10 @@ export interface AutomationTool { parameters?: Record; } +export type AutomationStep = typeof import('../constants/automation').AUTOMATION_STEPS[keyof typeof import('../constants/automation').AUTOMATION_STEPS]; + export interface AutomationStepData { - step: 'selection' | 'creation' | 'run'; + step: AutomationStep; mode?: AutomationMode; automation?: AutomationConfig; }