mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Merge branch 'V2' into feature/v2/filesButton
This commit is contained in:
commit
c5aa331ae8
6
frontend/.gitignore
vendored
6
frontend/.gitignore
vendored
@ -24,4 +24,8 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
playwright-report
|
||||
test-results
|
||||
test-results
|
||||
|
||||
# auto-generated files
|
||||
/src/assets/material-symbols-icons.json
|
||||
/src/assets/material-symbols-icons.d.ts
|
170
frontend/package-lock.json
generated
170
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
175
frontend/scripts/generate-icons.js
Normal file
175
frontend/scripts/generate-icons.js
Normal file
@ -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: <LocalIcon icon="icon-name" ...>
|
||||
const localIconMatches = content.match(/<LocalIcon\s+[^>]*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: <span className="material-symbols-rounded">icon-name</span>
|
||||
const spanMatches = content.match(/<span[^>]*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: <Icon icon="material-symbols:icon-name" ...>
|
||||
const iconMatches = content.match(/<Icon\s+[^>]*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<string, any>;
|
||||
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!`);
|
@ -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",
|
||||
|
@ -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)}
|
||||
>
|
||||
<AddIcon className="text-[var(--accent-interactive)]" />
|
||||
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
|
||||
{!isUploadHover && (
|
||||
<span>
|
||||
{t('landing.addFiles', 'Add Files')}
|
||||
@ -165,7 +165,7 @@ const LandingPage = () => {
|
||||
onClick={handleNativeUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<span className="material-symbols-rounded" style={{ fontSize: '1.25rem', color: 'var(--accent-interactive)' }}>upload</span>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
|
@ -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
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-rounded">language</span>
|
||||
<LocalIcon icon="language" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leftSection={<LanguageIcon style={{ fontSize: 18 }} />}
|
||||
leftSection={<LocalIcon icon="language" width="1.5rem" height="1.5rem" />}
|
||||
styles={{
|
||||
root: {
|
||||
border: 'none',
|
||||
|
52
frontend/src/components/shared/LocalIcon.tsx
Normal file
52
frontend/src/components/shared/LocalIcon.tsx
Normal file
@ -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<LocalIconProps> = ({ 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 <Icon icon={iconName} {...props} />;
|
||||
};
|
||||
|
||||
export default LocalIcon;
|
@ -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<HTMLDivElement>(({
|
||||
{
|
||||
id: 'read',
|
||||
name: t("quickAccess.read", "Read"),
|
||||
icon: <MenuBookIcon sx={{ fontSize: "1.5rem" }} />,
|
||||
icon: <LocalIcon icon="menu-book-rounded" width="1.5rem" height="1.5rem" />,
|
||||
size: 'lg',
|
||||
isRound: false,
|
||||
type: 'navigation',
|
||||
@ -54,28 +52,23 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
||||
handleReaderToggle();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sign',
|
||||
name: t("quickAccess.sign", "Sign"),
|
||||
icon:
|
||||
<span className="material-symbols-rounded font-size-20">
|
||||
signature
|
||||
</span>,
|
||||
size: 'lg',
|
||||
isRound: false,
|
||||
type: 'navigation',
|
||||
onClick: () => {
|
||||
setActiveButton('sign');
|
||||
handleToolSelect('sign');
|
||||
}
|
||||
},
|
||||
// TODO: Add sign
|
||||
//{
|
||||
// id: 'sign',
|
||||
// name: t("quickAccess.sign", "Sign"),
|
||||
// icon: <LocalIcon icon="signature-rounded" width="1.25rem" height="1.25rem" />,
|
||||
// size: 'lg',
|
||||
// isRound: false,
|
||||
// type: 'navigation',
|
||||
// onClick: () => {
|
||||
// setActiveButton('sign');
|
||||
// handleToolSelect('sign');
|
||||
// }
|
||||
//},
|
||||
{
|
||||
id: 'automate',
|
||||
name: t("quickAccess.automate", "Automate"),
|
||||
icon:
|
||||
<span className="material-symbols-rounded font-size-20">
|
||||
automation
|
||||
</span>,
|
||||
icon: <LocalIcon icon="automation-outline" width="1.25rem" height="1.25rem" />,
|
||||
size: 'lg',
|
||||
isRound: false,
|
||||
type: 'navigation',
|
||||
@ -87,28 +80,26 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
||||
{
|
||||
id: 'files',
|
||||
name: t("quickAccess.files", "Files"),
|
||||
icon: <FolderIcon sx={{ fontSize: "1.25rem" }} />,
|
||||
icon: <LocalIcon icon="folder-rounded" width="1.25rem" height="1.25rem" />,
|
||||
isRound: true,
|
||||
size: 'lg',
|
||||
type: 'modal',
|
||||
onClick: handleFilesButtonClick
|
||||
},
|
||||
{
|
||||
id: 'activity',
|
||||
name: t("quickAccess.activity", "Activity"),
|
||||
icon:
|
||||
<span className="material-symbols-rounded font-size-20">
|
||||
vital_signs
|
||||
</span>,
|
||||
isRound: true,
|
||||
size: 'lg',
|
||||
type: 'navigation',
|
||||
onClick: () => setActiveButton('activity')
|
||||
},
|
||||
//TODO: Activity
|
||||
//{
|
||||
// id: 'activity',
|
||||
// name: t("quickAccess.activity", "Activity"),
|
||||
// icon: <LocalIcon icon="vital-signs-rounded" width="1.25rem" height="1.25rem" />,
|
||||
// isRound: true,
|
||||
// size: 'lg',
|
||||
// type: 'navigation',
|
||||
// onClick: () => setActiveButton('activity')
|
||||
//},
|
||||
{
|
||||
id: 'config',
|
||||
name: t("quickAccess.config", "Config"),
|
||||
icon: <SettingsIcon sx={{ fontSize: "1rem" }} />,
|
||||
icon: <LocalIcon icon="settings-rounded" width="1.25rem" height="1.25rem" />,
|
||||
size: 'lg',
|
||||
type: 'modal',
|
||||
onClick: () => {
|
||||
@ -179,8 +170,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
||||
</div>
|
||||
|
||||
|
||||
{/* Add divider after Automate button (index 2) */}
|
||||
{index === 2 && (
|
||||
{/* Add divider after Automate button (index 1) and Files button (index 2) */}
|
||||
{index === 1 && (
|
||||
<Divider
|
||||
size="xs"
|
||||
className="content-divider"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { ActionIcon, Divider, Popover } from '@mantine/core';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import LocalIcon from './LocalIcon';
|
||||
import './rightRail/RightRail.css';
|
||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||
import { useRightRail } from '../../contexts/RightRailContext';
|
||||
@ -234,9 +234,7 @@ export default function RightRail() {
|
||||
onClick={handleSelectAll}
|
||||
disabled={currentView === 'viewer' || totalItems === 0 || selectedCount === totalItems}
|
||||
>
|
||||
<span className="material-symbols-rounded">
|
||||
select_all
|
||||
</span>
|
||||
<LocalIcon icon="select-all" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -251,9 +249,7 @@ export default function RightRail() {
|
||||
onClick={handleDeselectAll}
|
||||
disabled={currentView === 'viewer' || selectedCount === 0}
|
||||
>
|
||||
<span className="material-symbols-rounded">
|
||||
crop_square
|
||||
</span>
|
||||
<LocalIcon icon="crop-square-outline" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -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'}
|
||||
>
|
||||
<span className="material-symbols-rounded">
|
||||
pin_end
|
||||
</span>
|
||||
<LocalIcon icon="pin-end" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</Popover.Target>
|
||||
@ -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'}
|
||||
>
|
||||
<span className="material-symbols-rounded">delete</span>
|
||||
<LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</div>
|
||||
@ -331,7 +325,7 @@ export default function RightRail() {
|
||||
(currentView === 'pageEditor' && (activeFiles.length === 0 || !pageEditorFunctions?.closePdf))
|
||||
}
|
||||
>
|
||||
<CloseRoundedIcon />
|
||||
<LocalIcon icon="close-rounded" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -349,7 +343,7 @@ export default function RightRail() {
|
||||
className="right-rail-icon"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<span className="material-symbols-rounded">contrast</span>
|
||||
<LocalIcon icon="contrast" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
@ -368,9 +362,7 @@ export default function RightRail() {
|
||||
onClick={handleExportAll}
|
||||
disabled={currentView === 'viewer' || totalItems === 0}
|
||||
>
|
||||
<span className="material-symbols-rounded">
|
||||
download
|
||||
</span>
|
||||
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -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';
|
||||
|
||||
/**
|
||||
@ -30,6 +31,8 @@ export interface TextInputProps {
|
||||
readOnly?: boolean;
|
||||
/** Accessibility label */
|
||||
'aria-label'?: string;
|
||||
/** Focus event handler */
|
||||
onFocus?: () => void;
|
||||
}
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||
@ -45,6 +48,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
'aria-label': ariaLabel,
|
||||
onFocus,
|
||||
...props
|
||||
}, ref) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
@ -62,7 +66,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||
return (
|
||||
<div className={`${styles.container} ${className}`} style={style}>
|
||||
{icon && (
|
||||
<span
|
||||
<span
|
||||
className={styles.icon}
|
||||
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
|
||||
>
|
||||
@ -80,6 +84,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
aria-label={ariaLabel}
|
||||
onFocus={onFocus}
|
||||
style={{
|
||||
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
|
||||
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
|
||||
@ -96,7 +101,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
|
||||
aria-label="Clear input"
|
||||
>
|
||||
<span className="material-symbols-rounded">close</span>
|
||||
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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<TooltipProps> = ({
|
||||
}}
|
||||
title="Close tooltip"
|
||||
>
|
||||
<span className="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
|
||||
</button>
|
||||
)}
|
||||
{arrow && getArrowClass() && (
|
||||
|
@ -25,19 +25,39 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
||||
const quickAccessRef = useRef<HTMLDivElement>(null);
|
||||
const allToolsRef = useRef<HTMLDivElement>(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)`,
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
||||
{t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length})
|
||||
<Text size="sm" fw={500} mb="xs" style={{ color: "var(--mantine-color-text)" }}>
|
||||
{t("automate.creation.tools.selected", "Selected Tools")} ({tools.length})
|
||||
</Text>
|
||||
<Stack gap="0">
|
||||
{tools.map((tool, index) => (
|
||||
<React.Fragment key={tool.id}>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--mantine-color-gray-2)',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
position: 'relative',
|
||||
padding: 'var(--mantine-spacing-xs)'
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
borderRadius: tool.operation && !tool.configured
|
||||
? "var(--mantine-radius-lg) var(--mantine-radius-lg) 0 0"
|
||||
: "var(--mantine-radius-lg)",
|
||||
backgroundColor: "var(--mantine-color-gray-2)",
|
||||
position: "relative",
|
||||
padding: "var(--mantine-spacing-xs)",
|
||||
borderBottomWidth: tool.operation && !tool.configured ? "0" : "1px",
|
||||
}}
|
||||
>
|
||||
{/* 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)",
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ fontSize: 12 }} />
|
||||
<CloseIcon style={{ fontSize: 16 }} />
|
||||
</ActionIcon>
|
||||
|
||||
<div style={{ paddingRight: '1.25rem' }}>
|
||||
<div style={{ paddingRight: "1.25rem" }}>
|
||||
{/* Tool Selection Dropdown with inline settings cog */}
|
||||
<Group gap="xs" align="center" wrap="nowrap">
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<ToolSelector
|
||||
key={`tool-selector-${tool.id}`}
|
||||
onSelect={(newOperation) => 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)" }}
|
||||
>
|
||||
<SettingsIcon style={{ fontSize: 16 }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{/* Configuration status underneath */}
|
||||
{tool.operation && !tool.configured && (
|
||||
<Text pl="md" size="xs" c="dimmed" mt="xs">
|
||||
{t('automate.creation.tools.notConfigured', "! Not Configured")}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration status underneath */}
|
||||
{tool.operation && !tool.configured && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 var(--mantine-radius-lg) var(--mantine-radius-lg)",
|
||||
backgroundColor: "var(--active-bg)",
|
||||
padding: "var(--mantine-spacing-xs)",
|
||||
}}
|
||||
>
|
||||
<Text pl="md" size="xs" >
|
||||
{t("automate.creation.tools.notConfigured", "! Not Configured")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{index < tools.length - 1 && (
|
||||
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||
<Text size="xs" c="dimmed">↓</Text>
|
||||
<div style={{ textAlign: "center", padding: "8px 0" }}>
|
||||
<Text size="xs" c="dimmed">
|
||||
↓
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
@ -124,19 +138,23 @@ export default function ToolList({
|
||||
|
||||
{/* Arrow before Add Tool Button */}
|
||||
{tools.length > 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||
<Text size="xs" c="dimmed">↓</Text>
|
||||
<div style={{ textAlign: "center", padding: "8px 0" }}>
|
||||
<Text size="xs" c="dimmed">
|
||||
↓
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Tool Button */}
|
||||
<div style={{
|
||||
border: '1px solid var(--mantine-color-gray-2)',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<AutomationEntry
|
||||
title={t('automate.creation.tools.addTool', 'Add Tool')}
|
||||
title={t("automate.creation.tools.addTool", "Add Tool")}
|
||||
badgeIcon={AddCircleOutline}
|
||||
operations={[]}
|
||||
onClick={onToolAdd}
|
||||
@ -146,4 +164,4 @@ export default function ToolList({
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<HTMLDivElement>(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 (
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<Menu
|
||||
opened={opened}
|
||||
onChange={(isOpen) => {
|
||||
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 }}
|
||||
>
|
||||
<Menu.Target>
|
||||
<div style={{ width: '100%' }}>
|
||||
{selectedValue && toolRegistry[selectedValue] && !opened ? (
|
||||
// Show selected tool in AutomationEntry style when tool is selected and not searching
|
||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--mantine-spacing-sm)',
|
||||
padding: '0 0.5rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
}}>
|
||||
<div style={{ color: 'var(--mantine-color-text)', fontSize: '1.2rem' }}>
|
||||
{toolRegistry[selectedValue].icon}
|
||||
</div>
|
||||
<Text size="sm" style={{ flex: 1, color: 'var(--mantine-color-text)' }}>
|
||||
{toolRegistry[selectedValue].name}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Show search input when no tool selected or actively searching
|
||||
<ToolSearch
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
toolRegistry={filteredToolRegistry}
|
||||
mode="filter"
|
||||
placeholder={getDisplayValue()}
|
||||
hideIcon={true}
|
||||
onFocus={handleSearchFocus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Menu.Target>
|
||||
<div ref={containerRef} className='rounded-xl'>
|
||||
{/* Always show the target - either selected tool or search input */}
|
||||
|
||||
<Menu.Dropdown p={0} style={{ minWidth: '16rem' }}>
|
||||
<ScrollArea h={350}>
|
||||
<Stack gap="sm" p="sm">
|
||||
{displayGroups.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" p="md">
|
||||
{isSearching
|
||||
? t('tools.noSearchResults', 'No tools found')
|
||||
: t('tools.noTools', 'No tools available')
|
||||
}
|
||||
</Text>
|
||||
) : (
|
||||
renderedTools
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
{selectedValue && toolRegistry[selectedValue] && !opened ? (
|
||||
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
||||
borderRadius: "var(--mantine-radius-lg)" }}>
|
||||
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
|
||||
onSelect={()=>{}} rounded={true}></ToolButton>
|
||||
</div>
|
||||
) : (
|
||||
// Show search input when no tool selected OR when dropdown is opened
|
||||
<ToolSearch
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
toolRegistry={filteredToolRegistry}
|
||||
mode="unstyled"
|
||||
placeholder={getDisplayValue()}
|
||||
hideIcon={true}
|
||||
onFocus={handleInputFocus}
|
||||
autoFocus={shouldAutoFocus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Custom dropdown */}
|
||||
{opened && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
border: '1px solid var(--mantine-color-gray-3)',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
boxShadow: 'var(--mantine-shadow-sm)',
|
||||
marginTop: '4px',
|
||||
minWidth: '16rem'
|
||||
}}
|
||||
>
|
||||
<ScrollArea h={350}>
|
||||
<Stack gap="sm" p="sm">
|
||||
{displayGroups.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" p="md">
|
||||
{isSearching
|
||||
? t('tools.noSearchResults', 'No tools found')
|
||||
: t('tools.noTools', 'No tools available')
|
||||
}
|
||||
</Text>
|
||||
) : (
|
||||
renderedTools
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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 = (
|
||||
<Text fw={500} size="lg">
|
||||
{title}
|
||||
</Text>
|
||||
<span className="material-symbols-rounded" style={{ fontSize: '1.2rem', color: 'var(--icon-files-color)' }}>
|
||||
gpp_maybe
|
||||
</span>
|
||||
<LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
);
|
||||
@ -125,14 +122,12 @@ const ToolStep = ({
|
||||
</Flex>
|
||||
|
||||
{isCollapsed ? (
|
||||
<ChevronRightIcon style={{
|
||||
fontSize: '1.2rem',
|
||||
<LocalIcon icon="chevron-right-rounded" width="1.2rem" height="1.2rem" style={{
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
opacity: onCollapsedClick ? 1 : 0.5
|
||||
}} />
|
||||
) : (
|
||||
<ExpandMoreIcon style={{
|
||||
fontSize: '1.2rem',
|
||||
<LocalIcon icon="expand-more-rounded" width="1.2rem" height="1.2rem" style={{
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
opacity: onCollapsedClick ? 1 : 0.5
|
||||
}} />
|
||||
|
@ -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) {
|
||||
<Text fw={500} size="xl" p="md">
|
||||
{title}
|
||||
</Text>
|
||||
<span className="material-symbols-rounded" style={{ fontSize: '1.2rem', color: 'var(--icon-files-color)' }}>
|
||||
gpp_maybe
|
||||
</span>
|
||||
<LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
@ -9,9 +9,10 @@ interface ToolButtonProps {
|
||||
tool: ToolRegistryEntry;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => {
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ 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<ToolButtonProps> = ({ 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)',
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FitText
|
||||
text={tool.name}
|
||||
|
@ -76,4 +76,4 @@
|
||||
.search-input-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,22 @@
|
||||
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';
|
||||
import "./ToolPicker.css";
|
||||
|
||||
interface ToolSearchProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
toolRegistry: Readonly<Record<string, ToolRegistryEntry>>;
|
||||
onToolSelect?: (toolId: string) => void;
|
||||
mode: 'filter' | 'dropdown';
|
||||
mode: "filter" | "dropdown" | "unstyled";
|
||||
selectedToolKey?: string | null;
|
||||
placeholder?: string;
|
||||
hideIcon?: boolean;
|
||||
onFocus?: () => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const ToolSearch = ({
|
||||
@ -22,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);
|
||||
@ -37,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 }));
|
||||
@ -47,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);
|
||||
}
|
||||
};
|
||||
@ -63,49 +67,60 @@ 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 = (
|
||||
<div className="search-input-container">
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
value={value}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>}
|
||||
icon={hideIcon ? undefined : <LocalIcon icon="search-rounded" width="1.5rem" height="1.5rem" />}
|
||||
autoComplete="off"
|
||||
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (mode === 'filter') {
|
||||
if (mode === "filter") {
|
||||
return <div className="search-input-container">{searchInput}</div>;
|
||||
}
|
||||
|
||||
if (mode === "unstyled") {
|
||||
return searchInput;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={searchRef} style={{ position: 'relative' }}>
|
||||
<div ref={searchRef} style={{ position: "relative" }}>
|
||||
{searchInput}
|
||||
{dropdownOpen && filteredTools.length > 0 && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
border: '1px solid var(--mantine-color-gray-3)',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
backgroundColor: "var(--mantine-color-body)",
|
||||
border: "1px solid var(--mantine-color-gray-3)",
|
||||
borderRadius: "6px",
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||
maxHeight: "300px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Stack gap="xs" style={{ padding: '8px' }}>
|
||||
<Stack gap="xs" style={{ padding: "8px" }}>
|
||||
{filteredTools.map(({ id, tool }) => (
|
||||
<Button
|
||||
key={id}
|
||||
@ -114,22 +129,18 @@ const ToolSearch = ({
|
||||
onToolSelect && onToolSelect(id);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
leftSection={
|
||||
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
|
||||
{tool.icon}
|
||||
</div>
|
||||
}
|
||||
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ textAlign: "left" }}>
|
||||
<div style={{ fontWeight: 500 }}>{tool.name}</div>
|
||||
<Text size="xs" c="dimmed" style={{ marginTop: '2px' }}>
|
||||
<Text size="xs" c="dimmed" style={{ marginTop: "2px" }}>
|
||||
{tool.description}
|
||||
</Text>
|
||||
</div>
|
||||
|
@ -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 });
|
||||
|
@ -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: <span className="material-symbols-rounded">workspace_premium</span>,
|
||||
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">signature</span>,
|
||||
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.sign.title", "Sign"),
|
||||
component: null,
|
||||
view: "sign",
|
||||
@ -72,7 +73,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
// Document Security
|
||||
|
||||
"addPassword": {
|
||||
icon: <span className="material-symbols-rounded">password</span>,
|
||||
icon: <LocalIcon icon="password-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.addPassword.title", "Add Password"),
|
||||
component: AddPassword,
|
||||
view: "security",
|
||||
@ -85,7 +86,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: AddPasswordSettings
|
||||
},
|
||||
"watermark": {
|
||||
icon: <span className="material-symbols-rounded">branding_watermark</span>,
|
||||
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.watermark.title", "Add Watermark"),
|
||||
component: AddWatermark,
|
||||
view: "format",
|
||||
@ -98,7 +99,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: AddWatermarkSingleStepSettings
|
||||
},
|
||||
"add-stamp": {
|
||||
icon: <span className="material-symbols-rounded">approval</span>,
|
||||
icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">cleaning_services</span>,
|
||||
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.sanitize.title", "Sanitize"),
|
||||
component: Sanitize,
|
||||
view: "security",
|
||||
@ -120,7 +121,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: SanitizeSettings
|
||||
},
|
||||
"flatten": {
|
||||
icon: <span className="material-symbols-rounded">layers_clear</span>,
|
||||
icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">preview_off</span>,
|
||||
icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">license</span>,
|
||||
icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">lock</span>,
|
||||
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">fact_check</span>,
|
||||
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">verified</span>,
|
||||
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.validateSignature.title", "Validate PDF Signature"),
|
||||
component: null,
|
||||
view: "security",
|
||||
@ -188,7 +189,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
// Document Review
|
||||
|
||||
"read": {
|
||||
icon: <span className="material-symbols-rounded">article</span>,
|
||||
icon: <LocalIcon icon="article-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">assignment</span>,
|
||||
icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.changeMetadata.title", "Change Metadata"),
|
||||
component: null,
|
||||
view: "format",
|
||||
@ -208,7 +209,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
// Page Formatting
|
||||
|
||||
"cropPdf": {
|
||||
icon: <span className="material-symbols-rounded">crop</span>,
|
||||
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">rotate_right</span>,
|
||||
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.rotate.title", "Rotate"),
|
||||
component: null,
|
||||
view: "format",
|
||||
@ -226,7 +227,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING
|
||||
},
|
||||
"splitPdf": {
|
||||
icon: <span className="material-symbols-rounded">content_cut</span>,
|
||||
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.split.title", "Split"),
|
||||
component: SplitPdfPanel,
|
||||
view: "split",
|
||||
@ -237,7 +238,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: SplitSettings
|
||||
},
|
||||
"reorganize-pages": {
|
||||
icon: <span className="material-symbols-rounded">move_down</span>,
|
||||
icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">crop_free</span>,
|
||||
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">123</span>,
|
||||
icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">dashboard</span>,
|
||||
icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">looks_one</span>,
|
||||
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">attachment</span>,
|
||||
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.attachments.title", "Add Attachments"),
|
||||
component: null,
|
||||
view: "format",
|
||||
@ -298,7 +299,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
// Extraction
|
||||
|
||||
"extractPages": {
|
||||
icon: <span className="material-symbols-rounded">upload</span>,
|
||||
icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">filter</span>,
|
||||
icon: <LocalIcon icon="filter-alt" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.extractImages.title", "Extract Images"),
|
||||
component: null,
|
||||
view: "extract",
|
||||
@ -320,7 +321,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
// Removal
|
||||
|
||||
"removePages": {
|
||||
icon: <span className="material-symbols-rounded">delete</span>,
|
||||
icon: <LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">scan_delete</span>,
|
||||
icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">thread_unread</span>,
|
||||
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">remove_selection</span>,
|
||||
icon: <LocalIcon icon="remove-selection-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">lock_open_right</span>,
|
||||
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">remove_moderator</span>,
|
||||
icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.removeCertSign.title", "Remove Certificate Sign"),
|
||||
component: RemoveCertificateSign,
|
||||
view: "security",
|
||||
@ -385,7 +386,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
// Automation
|
||||
|
||||
"automate": {
|
||||
icon: <span className="material-symbols-rounded">automation</span>,
|
||||
icon: <LocalIcon icon="automation-outline" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">match_word</span>,
|
||||
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">split_scene_right</span>,
|
||||
icon: <LocalIcon icon="split-scene-right-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">content_cut</span>,
|
||||
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">palette</span>,
|
||||
icon: <LocalIcon icon="palette" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">build</span>,
|
||||
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">scanner</span>,
|
||||
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">layers</span>,
|
||||
icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">format_color_fill</span>,
|
||||
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">image</span>,
|
||||
icon: <LocalIcon icon="image-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">bookmark_add</span>,
|
||||
icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded">scanner</span>,
|
||||
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.fakeScan.title", "Scanner Effect"),
|
||||
component: null,
|
||||
view: "format",
|
||||
@ -507,7 +508,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
// Developer Tools
|
||||
|
||||
"show-javascript": {
|
||||
icon: <span className="material-symbols-rounded">javascript</span>,
|
||||
icon: <LocalIcon icon="javascript-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
|
||||
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: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
|
||||
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: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
|
||||
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: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
|
||||
name: t("home.devAirgapped.title", "Air-gapped Setup"),
|
||||
component: null,
|
||||
view: "external",
|
||||
@ -559,7 +560,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
|
||||
// Recommended Tools
|
||||
"compare": {
|
||||
icon: <span className="material-symbols-rounded">compare</span>,
|
||||
icon: <LocalIcon icon="compare-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.compare.title", "Compare"),
|
||||
component: null,
|
||||
view: "format",
|
||||
@ -568,7 +569,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
subcategoryId: SubcategoryId.GENERAL
|
||||
},
|
||||
"compress": {
|
||||
icon: <span className="material-symbols-rounded">zoom_in_map</span>,
|
||||
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.compress.title", "Compress"),
|
||||
component: CompressPdfPanel,
|
||||
view: "compress",
|
||||
@ -580,7 +581,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: CompressSettings
|
||||
},
|
||||
"convert": {
|
||||
icon: <span className="material-symbols-rounded">sync_alt</span>,
|
||||
icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.convert.title", "Convert"),
|
||||
component: ConvertPanel,
|
||||
view: "convert",
|
||||
@ -626,7 +627,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: ConvertSettings
|
||||
},
|
||||
"mergePdfs": {
|
||||
icon: <span className="material-symbols-rounded">library_add</span>,
|
||||
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.merge.title", "Merge"),
|
||||
component: null,
|
||||
view: "merge",
|
||||
@ -636,7 +637,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
maxFiles: -1
|
||||
},
|
||||
"multi-tool": {
|
||||
icon: <span className="material-symbols-rounded">dashboard_customize</span>,
|
||||
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.multiTool.title", "Multi-Tool"),
|
||||
component: null,
|
||||
view: "pageEditor",
|
||||
@ -646,7 +647,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
maxFiles: -1
|
||||
},
|
||||
"ocr": {
|
||||
icon: <span className="material-symbols-rounded">quick_reference_all</span>,
|
||||
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.ocr.title", "OCR"),
|
||||
component: OCRPanel,
|
||||
view: "convert",
|
||||
@ -658,7 +659,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: OCRSettings
|
||||
},
|
||||
"redact": {
|
||||
icon: <span className="material-symbols-rounded">visibility_off</span>,
|
||||
icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.redact.title", "Redact"),
|
||||
component: null,
|
||||
view: "redact",
|
||||
|
13
frontend/src/global.d.ts
vendored
13
frontend/src/global.d.ts
vendored
@ -4,4 +4,15 @@ declare module "../components/PageEditor";
|
||||
declare module "../components/Viewer";
|
||||
declare module "*.js";
|
||||
declare module '*.module.css';
|
||||
declare module 'pdfjs-dist';
|
||||
declare module 'pdfjs-dist';
|
||||
|
||||
// Auto-generated icon set JSON import
|
||||
declare module '../assets/material-symbols-icons.json' {
|
||||
const value: {
|
||||
prefix: string;
|
||||
icons: Record<string, any>;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
export default value;
|
||||
}
|
@ -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<AutomationTool[]>([]);
|
||||
|
||||
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<string, any> => {
|
||||
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
|
||||
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 = {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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";
|
||||
@ -13,14 +14,15 @@ 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) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
const { setMode } = useNavigation();
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(AUTOMATION_STEPS.SELECTION);
|
||||
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
|
||||
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
|
||||
|
||||
const automateOperation = useAutomateOperation();
|
||||
@ -62,7 +64,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
const renderCurrentStep = () => {
|
||||
switch (currentStep) {
|
||||
case 'selection':
|
||||
case AUTOMATION_STEPS.SELECTION:
|
||||
return (
|
||||
<AutomationSelection
|
||||
savedAutomations={savedAutomations}
|
||||
@ -80,7 +82,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
/>
|
||||
);
|
||||
|
||||
case 'creation':
|
||||
case AUTOMATION_STEPS.CREATION:
|
||||
if (!stepData.mode) {
|
||||
console.error('Creation mode is undefined');
|
||||
return null;
|
||||
@ -98,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;
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -24,8 +24,10 @@ export interface AutomationTool {
|
||||
parameters?: Record<string, any>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user