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/3] 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
to:
---
## 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/3] 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
+
) : (
}
+ leftSection={}
styles={{
root: {
border: 'none',
diff --git a/frontend/src/components/shared/LocalIcon.tsx b/frontend/src/components/shared/LocalIcon.tsx
new file mode 100644
index 000000000..c00ccefec
--- /dev/null
+++ b/frontend/src/components/shared/LocalIcon.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { addCollection, Icon } from '@iconify/react';
+import iconSet from '../../assets/material-symbols-icons.json';
+
+// Load icons synchronously at import time - guaranteed to be ready on first render
+let iconsLoaded = false;
+let localIconCount = 0;
+
+try {
+ if (iconSet) {
+ addCollection(iconSet);
+ iconsLoaded = true;
+ localIconCount = Object.keys(iconSet.icons || {}).length;
+ console.info(`✅ Local icons loaded: ${localIconCount} icons (${Math.round(JSON.stringify(iconSet).length / 1024)}KB)`);
+ }
+} catch (error) {
+ console.info('ℹ️ Local icons not available - using CDN fallback');
+}
+
+interface LocalIconProps {
+ icon: string;
+ width?: string | number;
+ height?: string | number;
+ style?: React.CSSProperties;
+ className?: string;
+}
+
+/**
+ * LocalIcon component that uses our locally bundled Material Symbols icons
+ * instead of loading from CDN
+ */
+export const LocalIcon: React.FC = ({ icon, ...props }) => {
+ // Convert our icon naming convention to the local collection format
+ const iconName = icon.startsWith('material-symbols:')
+ ? icon
+ : `material-symbols:${icon}`;
+
+ // Development logging (only in dev mode)
+ if (process.env.NODE_ENV === 'development') {
+ const logKey = `icon-${iconName}`;
+ if (!sessionStorage.getItem(logKey)) {
+ const source = iconsLoaded ? 'local' : 'CDN';
+ console.debug(`🎯 Icon: ${iconName} (${source})`);
+ sessionStorage.setItem(logKey, 'logged');
+ }
+ }
+
+ // Always render the icon - Iconify will use local if available, CDN if not
+ return ;
+};
+
+export default LocalIcon;
\ No newline at end of file
diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx
index 704c2f9bf..c57e49c40 100644
--- a/frontend/src/components/shared/QuickAccessBar.tsx
+++ b/frontend/src/components/shared/QuickAccessBar.tsx
@@ -1,9 +1,7 @@
import React, { useState, useRef, forwardRef, useEffect } from "react";
import { ActionIcon, Stack, Divider } from "@mantine/core";
import { useTranslation } from 'react-i18next';
-import MenuBookIcon from "@mui/icons-material/MenuBookRounded";
-import SettingsIcon from "@mui/icons-material/SettingsRounded";
-import FolderIcon from "@mui/icons-material/FolderRounded";
+import LocalIcon from './LocalIcon';
import { useRainbowThemeContext } from "./RainbowThemeProvider";
import AppConfigModal from './AppConfigModal';
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
@@ -44,7 +42,7 @@ const QuickAccessBar = forwardRef(({
{
id: 'read',
name: t("quickAccess.read", "Read"),
- icon: ,
+ icon: ,
size: 'lg',
isRound: false,
type: 'navigation',
@@ -54,29 +52,23 @@ const QuickAccessBar = forwardRef(({
handleReaderToggle();
}
},
- // TODO: Add sign tool
- // {
- // id: 'sign',
- // name: t("quickAccess.sign", "Sign"),
- // icon:
- //
- // signature
- // ,
- // size: 'lg',
- // isRound: false,
- // type: 'navigation',
- // onClick: () => {
- // setActiveButton('sign');
- // handleToolSelect('sign');
- // }
- // },
+ // TODO: Add sign
+ //{
+ // id: 'sign',
+ // name: t("quickAccess.sign", "Sign"),
+ // icon: ,
+ // size: 'lg',
+ // isRound: false,
+ // type: 'navigation',
+ // onClick: () => {
+ // setActiveButton('sign');
+ // handleToolSelect('sign');
+ // }
+ //},
{
id: 'automate',
name: t("quickAccess.automate", "Automate"),
- icon:
-
- automation
- ,
+ icon: ,
size: 'lg',
isRound: false,
type: 'navigation',
@@ -88,28 +80,26 @@ const QuickAccessBar = forwardRef(({
{
id: 'files',
name: t("quickAccess.files", "Files"),
- icon: ,
+ icon: ,
isRound: true,
size: 'lg',
type: 'modal',
onClick: handleFilesButtonClick
},
- {
- id: 'activity',
- name: t("quickAccess.activity", "Activity"),
- icon:
-
- vital_signs
- ,
- isRound: true,
- size: 'lg',
- type: 'navigation',
- onClick: () => setActiveButton('activity')
- },
+ //TODO: Activity
+ //{
+ // id: 'activity',
+ // name: t("quickAccess.activity", "Activity"),
+ // icon: ,
+ // isRound: true,
+ // size: 'lg',
+ // type: 'navigation',
+ // onClick: () => setActiveButton('activity')
+ //},
{
id: 'config',
name: t("quickAccess.config", "Config"),
- icon: ,
+ icon: ,
size: 'lg',
type: 'modal',
onClick: () => {
@@ -180,8 +170,8 @@ const QuickAccessBar = forwardRef(({
- {/* Add divider after Automate button (index 2) */}
- {index === 2 && (
+ {/* Add divider after Automate button (index 1) and Files button (index 2) */}
+ {index === 1 && (
-
- select_all
-
+
@@ -251,9 +249,7 @@ export default function RightRail() {
onClick={handleDeselectAll}
disabled={currentView === 'viewer' || selectedCount === 0}
>
-
- crop_square
-
+
@@ -273,9 +269,7 @@ export default function RightRail() {
disabled={!pageControlsVisible || totalItems === 0}
aria-label={typeof t === 'function' ? t('rightRail.selectByNumber', 'Select by Page Numbers') : 'Select by Page Numbers'}
>
-
- pin_end
-
+
@@ -309,7 +303,7 @@ export default function RightRail() {
disabled={!pageControlsVisible || (Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length === 0 : true)}
aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'}
>
- delete
+
@@ -331,7 +325,7 @@ export default function RightRail() {
(currentView === 'pageEditor' && (activeFiles.length === 0 || !pageEditorFunctions?.closePdf))
}
>
-
+
@@ -349,7 +343,7 @@ export default function RightRail() {
className="right-rail-icon"
onClick={toggleTheme}
>
- contrast
+
@@ -368,9 +362,7 @@ export default function RightRail() {
onClick={handleExportAll}
disabled={currentView === 'viewer' || totalItems === 0}
>
-
- download
-
+
diff --git a/frontend/src/components/shared/TextInput.tsx b/frontend/src/components/shared/TextInput.tsx
index e44e8efb2..fc3e99015 100644
--- a/frontend/src/components/shared/TextInput.tsx
+++ b/frontend/src/components/shared/TextInput.tsx
@@ -1,5 +1,6 @@
import React, { forwardRef } from 'react';
import { useMantineColorScheme } from '@mantine/core';
+import LocalIcon from './LocalIcon';
import styles from './textInput/TextInput.module.css';
/**
@@ -96,7 +97,7 @@ export const TextInput = forwardRef(({
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
aria-label="Clear input"
>
- close
+
)}
diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx
index 4c216d318..7940112ca 100644
--- a/frontend/src/components/shared/Tooltip.tsx
+++ b/frontend/src/components/shared/Tooltip.tsx
@@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
+import LocalIcon from './LocalIcon';
import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils';
import { useTooltipPosition } from '../../hooks/useTooltipPosition';
import { TooltipTip } from '../../types/tips';
@@ -171,9 +172,7 @@ export const Tooltip: React.FC = ({
}}
title="Close tooltip"
>
-
- close
-
+
)}
{arrow && getArrowClass() && (
diff --git a/frontend/src/components/tools/shared/ToolStep.tsx b/frontend/src/components/tools/shared/ToolStep.tsx
index c2ff1d5f6..9d86d2f03 100644
--- a/frontend/src/components/tools/shared/ToolStep.tsx
+++ b/frontend/src/components/tools/shared/ToolStep.tsx
@@ -1,7 +1,6 @@
import React, { createContext, useContext, useMemo, useRef } from 'react';
import { Text, Stack, Box, Flex, Divider } from '@mantine/core';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import ChevronRightIcon from '@mui/icons-material/ChevronRight';
+import LocalIcon from '../../shared/LocalIcon';
import { Tooltip } from '../../shared/Tooltip';
import { TooltipTip } from '../../../types/tips';
import { createFilesToolStep, FilesToolStepProps } from './FilesToolStep';
@@ -54,9 +53,7 @@ const renderTooltipTitle = (
{title}
-
- gpp_maybe
-
+
);
@@ -125,14 +122,12 @@ const ToolStep = ({
{isCollapsed ? (
-
) : (
-
diff --git a/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx
index 31c305d4a..6ed949442 100644
--- a/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx
+++ b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { Flex, Text, Divider } from '@mantine/core';
+import LocalIcon from '../../shared/LocalIcon';
import { Tooltip } from '../../shared/Tooltip';
export interface ToolWorkflowTitleProps {
@@ -29,9 +30,7 @@ export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) {
{title}
-
- gpp_maybe
-
+
diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx
index c17784a52..774126aa2 100644
--- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx
+++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx
@@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import { Stack, Button, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
+import LocalIcon from '../../shared/LocalIcon';
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { TextInput } from "../../shared/TextInput";
import './ToolPicker.css';
@@ -74,7 +75,7 @@ const ToolSearch = ({
value={value}
onChange={handleSearchChange}
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
- icon={hideIcon ? undefined : search}
+ icon={hideIcon ? undefined : }
autoComplete="off"
/>
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
index 3c3b5e89b..db8b2cf23 100644
--- a/frontend/src/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react';
+import LocalIcon from '../components/shared/LocalIcon';
import { useTranslation } from 'react-i18next';
import SplitPdfPanel from "../tools/Split";
import CompressPdfPanel from "../tools/Compress";
@@ -50,7 +51,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Signing
"certSign": {
- icon: workspace_premium,
+ icon: ,
name: t("home.certSign.title", "Sign with Certificate"),
component: null,
view: "sign",
@@ -59,7 +60,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.SIGNING
},
"sign": {
- icon: signature,
+ icon: ,
name: t("home.sign.title", "Sign"),
component: null,
view: "sign",
@@ -72,7 +73,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Document Security
"addPassword": {
- icon: password,
+ icon: ,
name: t("home.addPassword.title", "Add Password"),
component: AddPassword,
view: "security",
@@ -85,7 +86,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: AddPasswordSettings
},
"watermark": {
- icon: branding_watermark,
+ icon: ,
name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark,
view: "format",
@@ -98,7 +99,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: AddWatermarkSingleStepSettings
},
"add-stamp": {
- icon: approval,
+ icon: ,
name: t("home.AddStampRequest.title", "Add Stamp to PDF"),
component: null,
view: "format",
@@ -107,7 +108,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DOCUMENT_SECURITY
},
"sanitize": {
- icon: cleaning_services,
+ icon: ,
name: t("home.sanitize.title", "Sanitize"),
component: Sanitize,
view: "security",
@@ -120,7 +121,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: SanitizeSettings
},
"flatten": {
- icon: layers_clear,
+ icon: ,
name: t("home.flatten.title", "Flatten"),
component: null,
view: "format",
@@ -129,7 +130,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DOCUMENT_SECURITY
},
"unlock-pdf-forms": {
- icon: preview_off,
+ icon: ,
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
component: UnlockPdfForms,
view: "security",
@@ -142,7 +143,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: UnlockPdfFormsSettings
},
"manage-certificates": {
- icon: license,
+ icon: ,
name: t("home.manageCertificates.title", "Manage Certificates"),
component: null,
view: "security",
@@ -151,7 +152,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DOCUMENT_SECURITY
},
"change-permissions": {
- icon: lock,
+ icon: ,
name: t("home.changePermissions.title", "Change Permissions"),
component: ChangePermissions,
view: "security",
@@ -166,7 +167,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Verification
"get-all-info-on-pdf": {
- icon: fact_check,
+ icon: ,
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
component: null,
view: "extract",
@@ -175,7 +176,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.VERIFICATION
},
"validate-pdf-signature": {
- icon: verified,
+ icon: ,
name: t("home.validateSignature.title", "Validate PDF Signature"),
component: null,
view: "security",
@@ -188,7 +189,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Document Review
"read": {
- icon: article,
+ icon: ,
name: t("home.read.title", "Read"),
component: null,
view: "view",
@@ -197,7 +198,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DOCUMENT_REVIEW
},
"change-metadata": {
- icon: assignment,
+ icon: ,
name: t("home.changeMetadata.title", "Change Metadata"),
component: null,
view: "format",
@@ -208,7 +209,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Page Formatting
"cropPdf": {
- icon: crop,
+ icon: ,
name: t("home.crop.title", "Crop PDF"),
component: null,
view: "format",
@@ -217,7 +218,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING
},
"rotate": {
- icon: rotate_right,
+ icon: ,
name: t("home.rotate.title", "Rotate"),
component: null,
view: "format",
@@ -226,7 +227,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING
},
"splitPdf": {
- icon: content_cut,
+ icon: ,
name: t("home.split.title", "Split"),
component: SplitPdfPanel,
view: "split",
@@ -237,7 +238,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: SplitSettings
},
"reorganize-pages": {
- icon: move_down,
+ icon: ,
name: t("home.reorganizePages.title", "Reorganize Pages"),
component: null,
view: "pageEditor",
@@ -246,7 +247,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING
},
"adjust-page-size-scale": {
- icon: crop_free,
+ icon: ,
name: t("home.scalePages.title", "Adjust page size/scale"),
component: null,
view: "format",
@@ -255,7 +256,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING
},
"addPageNumbers": {
- icon: 123,
+ icon: ,
name: t("home.addPageNumbers.title", "Add Page Numbers"),
component: null,
view: "format",
@@ -264,7 +265,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING
},
"multi-page-layout": {
- icon: dashboard,
+ icon: ,
name: t("home.pageLayout.title", "Multi-Page Layout"),
component: null,
view: "format",
@@ -273,7 +274,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING
},
"single-large-page": {
- icon: looks_one,
+ icon: ,
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
component: SingleLargePage,
view: "format",
@@ -285,7 +286,7 @@ export function useFlatToolRegistry(): ToolRegistry {
operationConfig: singleLargePageOperationConfig
},
"add-attachments": {
- icon: attachment,
+ icon: ,
name: t("home.attachments.title", "Add Attachments"),
component: null,
view: "format",
@@ -298,7 +299,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Extraction
"extractPages": {
- icon: upload,
+ icon: ,
name: t("home.extractPages.title", "Extract Pages"),
component: null,
view: "extract",
@@ -307,7 +308,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.EXTRACTION
},
"extract-images": {
- icon: filter,
+ icon: ,
name: t("home.extractImages.title", "Extract Images"),
component: null,
view: "extract",
@@ -320,7 +321,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Removal
"removePages": {
- icon: delete,
+ icon: ,
name: t("home.removePages.title", "Remove Pages"),
component: null,
view: "remove",
@@ -329,7 +330,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL
},
"remove-blank-pages": {
- icon: scan_delete,
+ icon: ,
name: t("home.removeBlanks.title", "Remove Blank Pages"),
component: null,
view: "remove",
@@ -338,7 +339,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL
},
"remove-annotations": {
- icon: thread_unread,
+ icon: ,
name: t("home.removeAnnotations.title", "Remove Annotations"),
component: null,
view: "remove",
@@ -347,7 +348,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL
},
"remove-image": {
- icon: remove_selection,
+ icon: ,
name: t("home.removeImagePdf.title", "Remove Image"),
component: null,
view: "format",
@@ -356,7 +357,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL
},
"remove-password": {
- icon: lock_open_right,
+ icon: ,
name: t("home.removePassword.title", "Remove Password"),
component: RemovePassword,
view: "security",
@@ -369,7 +370,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: RemovePasswordSettings
},
"remove-certificate-sign": {
- icon: remove_moderator,
+ icon: ,
name: t("home.removeCertSign.title", "Remove Certificate Sign"),
component: RemoveCertificateSign,
view: "security",
@@ -385,7 +386,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Automation
"automate": {
- icon: automation,
+ icon: ,
name: t("home.automate.title", "Automate"),
component: React.lazy(() => import('../tools/Automate')),
view: "format",
@@ -396,7 +397,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["handleData"]
},
"auto-rename-pdf-file": {
- icon: match_word,
+ icon: ,
name: t("home.auto-rename.title", "Auto Rename PDF File"),
component: null,
view: "format",
@@ -405,7 +406,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.AUTOMATION
},
"auto-split-pages": {
- icon: split_scene_right,
+ icon: ,
name: t("home.autoSplitPDF.title", "Auto Split Pages"),
component: null,
view: "format",
@@ -414,7 +415,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.AUTOMATION
},
"auto-split-by-size-count": {
- icon: content_cut,
+ icon: ,
name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"),
component: null,
view: "format",
@@ -427,7 +428,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Advanced Formatting
"adjustContrast": {
- icon: palette,
+ icon: ,
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: null,
view: "format",
@@ -436,7 +437,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
},
"repair": {
- icon: build,
+ icon: ,
name: t("home.repair.title", "Repair"),
component: Repair,
view: "format",
@@ -449,7 +450,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: RepairSettings
},
"detect-split-scanned-photos": {
- icon: scanner,
+ icon: ,
name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"),
component: null,
view: "format",
@@ -458,7 +459,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
},
"overlay-pdfs": {
- icon: layers,
+ icon: ,
name: t("home.overlay-pdfs.title", "Overlay PDFs"),
component: null,
view: "format",
@@ -467,7 +468,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
},
"replace-and-invert-color": {
- icon: format_color_fill,
+ icon: ,
name: t("home.replaceColorPdf.title", "Replace & Invert Color"),
component: null,
view: "format",
@@ -476,7 +477,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
},
"add-image": {
- icon: image,
+ icon: ,
name: t("home.addImage.title", "Add Image"),
component: null,
view: "format",
@@ -485,7 +486,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
},
"edit-table-of-contents": {
- icon: bookmark_add,
+ icon: ,
name: t("home.editTableOfContents.title", "Edit Table of Contents"),
component: null,
view: "format",
@@ -494,7 +495,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
},
"scanner-effect": {
- icon: scanner,
+ icon: ,
name: t("home.fakeScan.title", "Scanner Effect"),
component: null,
view: "format",
@@ -507,7 +508,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Developer Tools
"show-javascript": {
- icon: javascript,
+ icon: ,
name: t("home.showJS.title", "Show JavaScript"),
component: null,
view: "extract",
@@ -516,7 +517,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DEVELOPER_TOOLS
},
"dev-api": {
- icon: open_in_new,
+ icon: ,
name: t("home.devApi.title", "API"),
component: null,
view: "external",
@@ -526,7 +527,7 @@ export function useFlatToolRegistry(): ToolRegistry {
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html"
},
"dev-folder-scanning": {
- icon: open_in_new,
+ icon: ,
name: t("home.devFolderScanning.title", "Automated Folder Scanning"),
component: null,
view: "external",
@@ -536,7 +537,7 @@ export function useFlatToolRegistry(): ToolRegistry {
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/"
},
"dev-sso-guide": {
- icon: open_in_new,
+ icon: ,
name: t("home.devSsoGuide.title", "SSO Guide"),
component: null,
view: "external",
@@ -546,7 +547,7 @@ export function useFlatToolRegistry(): ToolRegistry {
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
},
"dev-airgapped": {
- icon: open_in_new,
+ icon: ,
name: t("home.devAirgapped.title", "Air-gapped Setup"),
component: null,
view: "external",
@@ -559,7 +560,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Recommended Tools
"compare": {
- icon: compare,
+ icon: ,
name: t("home.compare.title", "Compare"),
component: null,
view: "format",
@@ -568,7 +569,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.GENERAL
},
"compress": {
- icon: zoom_in_map,
+ icon: ,
name: t("home.compress.title", "Compress"),
component: CompressPdfPanel,
view: "compress",
@@ -580,7 +581,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: CompressSettings
},
"convert": {
- icon: sync_alt,
+ icon: ,
name: t("home.convert.title", "Convert"),
component: ConvertPanel,
view: "convert",
@@ -626,7 +627,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: ConvertSettings
},
"mergePdfs": {
- icon: library_add,
+ icon: ,
name: t("home.merge.title", "Merge"),
component: null,
view: "merge",
@@ -636,7 +637,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1
},
"multi-tool": {
- icon: dashboard_customize,
+ icon: ,
name: t("home.multiTool.title", "Multi-Tool"),
component: null,
view: "pageEditor",
@@ -646,7 +647,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1
},
"ocr": {
- icon: quick_reference_all,
+ icon: ,
name: t("home.ocr.title", "OCR"),
component: OCRPanel,
view: "convert",
@@ -658,7 +659,7 @@ export function useFlatToolRegistry(): ToolRegistry {
settingsComponent: OCRSettings
},
"redact": {
- icon: visibility_off,
+ icon: ,
name: t("home.redact.title", "Redact"),
component: null,
view: "redact",
diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts
index eb4b5d6c2..5511059a8 100644
--- a/frontend/src/global.d.ts
+++ b/frontend/src/global.d.ts
@@ -4,4 +4,15 @@ declare module "../components/PageEditor";
declare module "../components/Viewer";
declare module "*.js";
declare module '*.module.css';
-declare module 'pdfjs-dist';
\ No newline at end of file
+declare module 'pdfjs-dist';
+
+// Auto-generated icon set JSON import
+declare module '../assets/material-symbols-icons.json' {
+ const value: {
+ prefix: string;
+ icons: Record;
+ width?: number;
+ height?: number;
+ };
+ export default value;
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
index 9ddce1e0b..006c9f179 100644
--- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
+++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
@@ -1,11 +1,15 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import StarIcon from '@mui/icons-material/Star';
-import CompressIcon from '@mui/icons-material/Compress';
-import SecurityIcon from '@mui/icons-material/Security';
-import TextFieldsIcon from '@mui/icons-material/TextFields';
+import React from 'react';
+import LocalIcon from '../../../components/shared/LocalIcon';
import { SuggestedAutomation } from '../../../types/automation';
+// Create icon components
+const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' });
+const TextFieldsIcon = () => React.createElement(LocalIcon, { icon: 'text-fields', width: '1.5rem', height: '1.5rem' });
+const SecurityIcon = () => React.createElement(LocalIcon, { icon: 'security', width: '1.5rem', height: '1.5rem' });
+const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1.5rem', height: '1.5rem' });
+
export function useSuggestedAutomations(): SuggestedAutomation[] {
const { t } = useTranslation();
diff --git a/frontend/src/index.css b/frontend/src/index.css
index f7e5e0865..ec2585e8c 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,9 +1,3 @@
-@import 'material-symbols/rounded.css';
-
-.material-symbols-rounded {
- font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
-}
-
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 215a9378b..6886183a1 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -42,7 +42,7 @@
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
- // "resolveJsonModule": true, /* Enable importing .json files. */
+ "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
From ed61c71db7badd5cc7985db57dd1a33305aa70cc Mon Sep 17 00:00:00 2001
From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:10:51 +0100
Subject: [PATCH 3/3] 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",