diff --git a/frontend/.gitignore b/frontend/.gitignore index 8b055b7a6..1191bbebf 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -24,4 +24,8 @@ yarn-debug.log* yarn-error.log* playwright-report -test-results \ No newline at end of file +test-results + +# auto-generated files +/src/assets/material-symbols-icons.json +/src/assets/material-symbols-icons.d.ts \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f9ec204a6..817f7b17e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@iconify/react": "^6.0.0", "@mantine/core": "^8.0.1", "@mantine/dropzone": "^8.0.1", "@mantine/hooks": "^8.0.1", @@ -29,7 +30,6 @@ "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", - "material-symbols": "^0.33.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", "react": "^19.1.0", @@ -40,6 +40,8 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@iconify-json/material-symbols": "^1.2.33", + "@iconify/utils": "^3.0.1", "@playwright/test": "^1.40.0", "@types/node": "^24.2.1", "@types/react": "^19.1.4", @@ -48,6 +50,7 @@ "@vitest/coverage-v8": "^1.0.0", "jsdom": "^23.0.0", "license-checker": "^25.0.1", + "madge": "^8.0.0", "postcss": "^8.5.3", "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.17.0", @@ -88,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", @@ -331,12 +356,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", - "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -419,9 +444,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -553,6 +578,20 @@ "node": ">=18" } }, + "node_modules/@dependents/detective-less": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.1.tgz", + "integrity": "sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -1177,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", @@ -2367,6 +2504,96 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@ts-graphviz/adapter": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ts-graphviz/adapter/-/adapter-2.0.6.tgz", + "integrity": "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/ast": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/ast/-/ast-2.0.7.tgz", + "integrity": "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/common": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ts-graphviz/common/-/common-2.1.5.tgz", + "integrity": "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/core": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/core/-/core-2.0.7.tgz", + "integrity": "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -2475,6 +2702,132 @@ "@types/react": "*" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", @@ -2668,6 +3021,94 @@ "dev": true, "license": "MIT" }, + "node_modules/@vue/compiler-core": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.19.tgz", + "integrity": "sha512-/afpyvlkrSNYbPo94Qu8GtIOWS+g5TRdOvs6XZNw6pWQQmj5pBgSZvEPOIZlqWq0YvoUhDDQaQ2TnzuJdOV4hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.19", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.19.tgz", + "integrity": "sha512-Drs6rPHQZx/pN9S6ml3Z3K/TWCIRPvzG2B/o5kFK9X0MNHt8/E+38tiRfojufrYBfA6FQUFB2qBBRXlcSXWtOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.19", + "@vue/shared": "3.5.19" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.19.tgz", + "integrity": "sha512-YWCm1CYaJ+2RvNmhCwI7t3I3nU+hOrWGWMsn+Z/kmm1jy5iinnVtlmkiZwbLlbV1SRizX7vHsc0/bG5dj0zRTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.19", + "@vue/compiler-dom": "3.5.19", + "@vue/compiler-ssr": "3.5.19", + "@vue/shared": "3.5.19", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.19.tgz", + "integrity": "sha512-/wx0VZtkWOPdiQLWPeQeqpHWR/LuNC7bHfSX7OayBTtUy8wur6vT6EQIX6Et86aED6J+y8tTw43qo2uoqGg5sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.19", + "@vue/shared": "3.5.19" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.19.tgz", + "integrity": "sha512-IhXCOn08wgKrLQxRFKKlSacWg4Goi1BolrdEeLYn6tgHjJNXVrWJ5nzoxZqNwl5p88aLlQ8LOaoMa3AYvaKJ/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2738,6 +3179,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2752,6 +3200,13 @@ "node": ">= 8" } }, + "node_modules/app-module-path": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", + "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -2808,6 +3263,16 @@ "node": "*" } }, + "node_modules/ast-module-types": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-6.0.1.tgz", + "integrity": "sha512-WHw67kLXYbZuHTmcdbIrVArCq5wxo6NEuj3hiYAWr8mwJeC+C2mMCIBIWCiDoCye/OF/xelc+teJ1ERoWmnEIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2893,6 +3358,27 @@ "devOptional": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -2922,6 +3408,18 @@ "integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==", "license": "MIT" }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2978,6 +3476,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3152,6 +3675,42 @@ "node": ">=10" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3201,6 +3760,23 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3424,6 +4000,29 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3450,6 +4049,35 @@ "node": ">=4" } }, + "node_modules/dependency-tree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.2.0.tgz", + "integrity": "sha512-+C1H3mXhcvMCeu5i2Jpg9dc0N29TWTuT6vJD7mHLAfVmAbo9zW8NlkvQ1tYd3PDMab0IRQM0ccoyX68EZtx9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "filing-cabinet": "^5.0.3", + "precinct": "^12.2.0", + "typescript": "^5.8.3" + }, + "bin": { + "dependency-tree": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-tree/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3474,6 +4102,147 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/detective-amd": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-6.0.1.tgz", + "integrity": "sha512-TtyZ3OhwUoEEIhTFoc1C9IyJIud3y+xYkSRjmvCt65+ycQuc3VcBrPRTMWoO/AnuCyOB8T5gky+xf7Igxtjd3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "escodegen": "^2.1.0", + "get-amd-module-type": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "bin": { + "detective-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-cjs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.0.1.tgz", + "integrity": "sha512-tLTQsWvd2WMcmn/60T2inEJNhJoi7a//PQ7DwRKEj1yEeiQs4mrONgsUtEJKnZmrGWBBmE0kJ1vqOG/NAxwaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-es6": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-5.0.1.tgz", + "integrity": "sha512-XusTPuewnSUdoxRSx8OOI6xIA/uld/wMQwYsouvFN2LAg7HgP06NF1lHRV3x6BZxyL2Kkoih4ewcq8hcbGtwew==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-postcss": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detective-postcss/-/detective-postcss-7.0.1.tgz", + "integrity": "sha512-bEOVpHU9picRZux5XnwGsmCN4+8oZo7vSW0O0/Enq/TO5R2pIAP2279NsszpJR7ocnQt4WXU0+nnh/0JuK4KHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-url": "^1.2.4", + "postcss-values-parser": "^6.0.2" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.47" + } + }, + "node_modules/detective-sass": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-sass/-/detective-sass-6.0.1.tgz", + "integrity": "sha512-jSGPO8QDy7K7pztUmGC6aiHkexBQT4GIH+mBAL9ZyBmnUIOFbkfZnO8wPRRJFP/QP83irObgsZHCoDHZ173tRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-scss": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-scss/-/detective-scss-5.0.1.tgz", + "integrity": "sha512-MAyPYRgS6DCiS6n6AoSBJXLGVOydsr9huwXORUlJ37K3YLyiN0vYHpzs3AdJOgHobBfispokoqrEon9rbmKacg==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-stylus": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-stylus/-/detective-stylus-5.0.1.tgz", + "integrity": "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-typescript": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-14.0.0.tgz", + "integrity": "sha512-pgN43/80MmWVSEi5LUuiVvO/0a9ss5V7fwVfrJ4QzAQRd3cwqU1SfWGXJFcNKUqoD5cS+uIovhw5t/0rSeC5Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "^8.23.0", + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4" + } + }, + "node_modules/detective-vue2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/detective-vue2/-/detective-vue2-2.2.0.tgz", + "integrity": "sha512-sVg/t6O2z1zna8a/UIV6xL5KUa2cMTQbdTIIvqNM0NIPswp52fe43Nwmbahzj3ww4D844u/vC2PYfiGLvD3zFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^5.0.1", + "@vue/compiler-sfc": "^3.5.13", + "detective-es6": "^5.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -3672,6 +4441,76 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -3682,6 +4521,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -3719,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", @@ -3771,6 +4626,42 @@ "node": ">= 12" } }, + "node_modules/filing-cabinet": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.0.3.tgz", + "integrity": "sha512-PlPcMwVWg60NQkhvfoxZs4wEHjhlOO/y7OAm4sKM60o1Z9nttRY4mcdQxp/iZ+kg/Vv6Hw1OAaTbYVM9DA9pYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-module-path": "^2.2.0", + "commander": "^12.1.0", + "enhanced-resolve": "^5.18.0", + "module-definition": "^6.0.1", + "module-lookup-amd": "^9.0.3", + "resolve": "^1.22.10", + "resolve-dependency-path": "^4.0.1", + "sass-lookup": "^6.1.0", + "stylus-lookup": "^6.1.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3" + }, + "bin": { + "filing-cabinet": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/filing-cabinet/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3934,6 +4825,20 @@ "node": ">=6.9.0" } }, + "node_modules/get-amd-module-type": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-6.0.1.tgz", + "integrity": "sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3987,6 +4892,13 @@ "node": ">=6" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4044,6 +4956,22 @@ "node": ">=4" } }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4273,6 +5201,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -4321,6 +5270,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4388,6 +5344,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4398,6 +5364,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4405,6 +5381,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -4418,6 +5404,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-url-superb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", + "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -4662,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", @@ -5062,6 +6087,23 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5103,6 +6145,45 @@ "lz-string": "bin/bin.js" } }, + "node_modules/madge": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/madge/-/madge-8.0.0.tgz", + "integrity": "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^7.2.0", + "commondir": "^1.0.1", + "debug": "^4.3.4", + "dependency-tree": "^11.0.0", + "ora": "^5.4.1", + "pluralize": "^8.0.0", + "pretty-ms": "^7.0.1", + "rc": "^1.2.8", + "stream-to-array": "^2.3.0", + "ts-graphviz": "^2.1.2", + "walkdir": "^0.4.1" + }, + "bin": { + "madge": "bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.me/pahen" + }, + "peerDependencies": { + "typescript": "^5.4.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -5150,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", @@ -5359,6 +6434,52 @@ "dev": true, "license": "MIT" }, + "node_modules/module-definition": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.1.tgz", + "integrity": "sha512-FeVc50FTfVVQnolk/WQT8MX+2WVcDnTGiq6Wo+/+lJ2ET1bRVi3HG3YlJUfqagNMc/kUlFSoR96AJkxGpKz13g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "bin": { + "module-definition": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-lookup-amd": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-9.0.5.tgz", + "integrity": "sha512-Rs5FVpVcBYRHPLuhHOjgbRhosaQYLtEo3JIeDIbmNo7mSssi1CTzwMh8v36gAzpbzLGXI9wB/yHh+5+3fY1QVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "glob": "^7.2.3", + "requirejs": "^2.3.7", + "requirejs-config-file": "^4.0.0" + }, + "bin": { + "lookup-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-lookup-amd/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5438,6 +6559,19 @@ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "license": "MIT" }, + "node_modules/node-source-walk": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.1.tgz", + "integrity": "sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.7" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -5578,6 +6712,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -5623,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", @@ -5658,6 +6822,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -5857,10 +7031,20 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5877,7 +7061,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6186,6 +7370,64 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postcss-values-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "color-name": "^1.1.4", + "is-url-superb": "^4.0.0", + "quote-unquote": "^1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.2.9" + } + }, + "node_modules/precinct": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", + "integrity": "sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^5.0.1", + "commander": "^12.1.0", + "detective-amd": "^6.0.1", + "detective-cjs": "^6.0.1", + "detective-es6": "^5.0.1", + "detective-postcss": "^7.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.0.0", + "detective-vue2": "^2.2.0", + "module-definition": "^6.0.1", + "node-source-walk": "^7.0.1", + "postcss": "^8.5.1", + "typescript": "^5.7.3" + }, + "bin": { + "precinct": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/precinct/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -6228,6 +7470,22 @@ "node": ">= 0.8" } }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6279,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", @@ -6307,12 +7581,35 @@ ], "license": "MIT" }, + "node_modules/quote-unquote": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", + "dev": true, + "license": "MIT" + }, "node_modules/raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -6596,8 +7893,8 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6666,6 +7963,34 @@ "node": ">=0.10.0" } }, + "node_modules/requirejs": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz", + "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==", + "dev": true, + "license": "MIT", + "bin": { + "r_js": "bin/r.js", + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/requirejs-config-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz", + "integrity": "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esprima": "^4.0.0", + "stringify-object": "^3.2.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -6693,6 +8018,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dependency-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/resolve-dependency-path/-/resolve-dependency-path-4.0.1.tgz", + "integrity": "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6702,6 +8037,46 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6805,6 +8180,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -6819,8 +8195,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -6829,6 +8204,33 @@ "dev": true, "license": "MIT" }, + "node_modules/sass-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.0.tgz", + "integrity": "sha512-Zx+lVyoWqXZxHuYWlTA17Z5sczJ6braNT2C7rmClw+c4E7r/n911Zwss3h1uHI9reR5AgHZyNHF7c2+VIp5AUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "enhanced-resolve": "^5.18.0" + }, + "bin": { + "sass-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sass-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -6913,8 +8315,8 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -7050,12 +8452,22 @@ "dev": true, "license": "MIT" }, + "node_modules/stream-to-array": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", + "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -7082,6 +8494,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7095,6 +8522,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -7120,6 +8557,16 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-literal": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", @@ -7146,6 +8593,32 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, + "node_modules/stylus-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/stylus-lookup/-/stylus-lookup-6.1.0.tgz", + "integrity": "sha512-5QSwgxAzXPMN+yugy61C60PhoANdItfdjSEZR8siFwz7yL9jTmV0UBKDCfn3K8GkGB4g0Y9py7vTCX8rFu4/pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0" + }, + "bin": { + "stylus-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylus-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/sugarss": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", @@ -7292,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", @@ -7418,6 +8897,60 @@ "node": ">=0.6" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-graphviz": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-2.1.6.tgz", + "integrity": "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/adapter": "^2.0.6", + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5", + "@ts-graphviz/core": "^2.0.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8827,6 +10360,26 @@ "node": ">=18" } }, + "node_modules/walkdir": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-vitals": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index ad945dbc2..eaa5f20d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@iconify/react": "^6.0.0", "@mantine/core": "^8.0.1", "@mantine/dropzone": "^8.0.1", "@mantine/hooks": "^8.0.1", @@ -25,7 +26,6 @@ "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", - "material-symbols": "^0.33.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", "react": "^19.1.0", @@ -36,10 +36,14 @@ "web-vitals": "^2.1.4" }, "scripts": { + "predev": "npm run generate-icons", "dev": "npx tsc --noEmit && vite", + "prebuild": "npm run generate-icons", "build": "npx tsc --noEmit && vite build", "preview": "vite preview", "generate-licenses": "node scripts/generate-licenses.js", + "generate-icons": "node scripts/generate-icons.js", + "generate-icons:verbose": "node scripts/generate-icons.js --verbose", "test": "vitest", "test:watch": "vitest --watch", "test:coverage": "vitest --coverage", @@ -66,6 +70,8 @@ ] }, "devDependencies": { + "@iconify-json/material-symbols": "^1.2.33", + "@iconify/utils": "^3.0.1", "@playwright/test": "^1.40.0", "@types/node": "^24.2.1", "@types/react": "^19.1.4", @@ -74,6 +80,7 @@ "@vitest/coverage-v8": "^1.0.0", "jsdom": "^23.0.0", "license-checker": "^25.0.1", + "madge": "^8.0.0", "postcss": "^8.5.3", "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.17.0", diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index e09f874ac..2c9a0a6cd 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -85,6 +85,7 @@ "warning": { "tooltipTitle": "Warning" }, + "edit": "Edit", "delete": "Delete", "username": "Username", "password": "Password", @@ -538,10 +539,6 @@ "title": "Edit Table of Contents", "desc": "Add or edit bookmarks and table of contents in PDF documents" }, - "automate": { - "title": "Automate", - "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." - }, "manageCertificates": { "title": "Manage Certificates", "desc": "Import, export, or delete digital certificate files used for signing PDFs." @@ -601,8 +598,16 @@ "changePermissions": { "title": "Change Permissions", "desc": "Change document restrictions and permissions" + }, + "automate": { + "title": "Automate", + "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." } }, + "landing": { + "addFiles": "Add Files", + "uploadFromComputer": "Upload from computer" + }, "viewPdf": { "tags": "view,read,annotate,text,image,highlight,edit", "title": "View/Edit PDF", @@ -731,7 +736,8 @@ "officeDocs": "Office Documents (Word, Excel, PowerPoint)", "imagesExt": "Images (JPG, PNG, etc.)", "markdown": "Markdown", - "textRtf": "Text/RTF" + "textRtf": "Text/RTF", + "grayscale": "Greyscale" }, "imageToPdf": { "tags": "conversion,img,jpg,picture,photo" @@ -986,6 +992,7 @@ }, "submit": "Change" }, + "removePages": { "tags": "Remove pages,delete pages", "title": "Remove Pages", @@ -1920,23 +1927,41 @@ "currentPage": "Current Page", "totalPages": "Total Pages" }, + "rightRail": { + "closeSelected": "Close Selected Files", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "selectByNumber": "Select by Page Numbers", + "deleteSelected": "Delete Selected Pages", + "closePdf": "Close PDF", + "exportAll": "Export PDF", + "downloadSelected": "Download Selected Files", + "downloadAll": "Download All", + "toggleTheme": "Toggle Theme", + "language": "Language" + }, "toolPicker": { "searchPlaceholder": "Search tools...", "noToolsFound": "No tools found", "allTools": "ALL TOOLS", "quickAccess": "QUICK ACCESS", + "categories": { + "standardTools": "Standard Tools", + "advancedTools": "Advanced Tools", + "recommendedTools": "Recommended Tools" + }, "subcategories": { - "Signing": "Signing", - "Document Security": "Document Security", - "Verification": "Verification", - "Document Review": "Document Review", - "Page Formatting": "Page Formatting", - "Extraction": "Extraction", - "Removal": "Removal", - "Automation": "Automation", - "General": "General", - "Advanced Formatting": "Advanced Formatting", - "Developer Tools": "Developer Tools" + "signing": "Signing", + "documentSecurity": "Document Security", + "verification": "Verification", + "documentReview": "Document Review", + "pageFormatting": "Page Formatting", + "extraction": "Extraction", + "removal": "Removal", + "automation": "Automation", + "general": "General", + "advancedFormatting": "Advanced Formatting", + "developerTools": "Developer Tools" } }, "quickAccess": { @@ -1963,6 +1988,7 @@ "dropFilesHere": "Drop files here or click to upload", "pdfFilesOnly": "PDF files only", "supportedFileTypes": "Supported file types", + "upload": "Upload", "uploadFile": "Upload File", "uploadFiles": "Upload Files", "noFilesInStorage": "No files available in storage. Upload some files first.", @@ -2016,7 +2042,8 @@ "downloadSelected": "Download Selected", "selectedCount": "{{count}} selected", "download": "Download", - "delete": "Delete" + "delete": "Delete", + "unsupported":"Unsupported" }, "storage": { "temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically", @@ -2186,5 +2213,68 @@ "results": { "title": "Decrypted PDFs" } - } + }, + "automate": { + "title": "Automate", + "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks.", + "invalidStep": "Invalid step", + "files": { + "placeholder": "Select files to process with this automation" + }, + "selection": { + "title": "Automation Selection", + "saved": { + "title": "Saved" + }, + "createNew": { + "title": "Create New Automation" + }, + "suggested": { + "title": "Suggested" + } + }, + "creation": { + "createTitle": "Create Automation", + "editTitle": "Edit Automation", + "description": "Automations run tools sequentially. To get started, add tools in the order you want them to run.", + "name": { + "placeholder": "Automation name" + }, + "tools": { + "selectTool": "Select a tool...", + "selected": "Selected Tools", + "remove": "Remove tool", + "configure": "Configure tool", + "notConfigured": "! Not Configured", + "addTool": "Add Tool", + "add": "Add a tool..." + }, + "save": "Save Automation", + "unsavedChanges": { + "title": "Unsaved Changes", + "message": "You have unsaved changes. Are you sure you want to go back? All changes will be lost.", + "cancel": "Cancel", + "confirm": "Go Back" + } + }, + "run": { + "title": "Run Automation" + }, + "sequence": { + "unnamed": "Unnamed Automation", + "steps": "{{count}} steps", + "running": "Running Automation...", + "run": "Run Automation", + "finish": "Finish" + }, + "reviewTitle": "Automation Results", + "config": { + "loading": "Loading tool configuration...", + "noSettings": "This tool does not have configurable settings.", + "title": "Configure {{toolName}}", + "description": "Configure the settings for this tool. These settings will be applied when the automation runs.", + "cancel": "Cancel", + "save": "Save Configuration" + } + } } diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 358ccd53a..26c2e5b15 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -55,6 +55,7 @@ "bored": "Bored Waiting?", "alphabet": "Alphabet", "downloadPdf": "Download PDF", + "text": "Text", "font": "Font", "selectFillter": "-- Select --", @@ -607,6 +608,10 @@ "desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size" } }, + "landing": { + "addFiles": "Add Files", + "uploadFromComputer": "Upload from computer" + }, "viewPdf": { "tags": "view,read,annotate,text,image,highlight,edit", "title": "View/Edit PDF", @@ -2068,6 +2073,18 @@ } } }, + "rightRail": { + "closePdf": "Close PDF", + "closeSelected": "Close Selected Files", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "selectByNumber": "Select by Page Numbers", + "deleteSelected": "Delete Selected Pages", + "toggleTheme": "Toggle Theme", + "exportAll": "Export PDF", + "downloadSelected": "Download Selected Files", + "downloadAll": "Download All" + }, "removePassword": { "title": "Remove Password", "desc": "Remove password protection from your PDF document.", diff --git a/frontend/scripts/generate-icons.js b/frontend/scripts/generate-icons.js new file mode 100644 index 000000000..681b06728 --- /dev/null +++ b/frontend/scripts/generate-icons.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +const { icons } = require('@iconify-json/material-symbols'); +const { getIcons } = require('@iconify/utils'); +const fs = require('fs'); +const path = require('path'); + +// Check for verbose flag +const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v'); + +// Logging functions +const info = (message) => console.log(message); +const debug = (message) => { + if (isVerbose) { + console.log(message); + } +}; + +// Function to scan codebase for LocalIcon usage +function scanForUsedIcons() { + const usedIcons = new Set(); + const srcDir = path.join(__dirname, '..', 'src'); + + info('🔍 Scanning codebase for LocalIcon usage...'); + + if (!fs.existsSync(srcDir)) { + console.error('❌ Source directory not found:', srcDir); + process.exit(1); + } + + // Recursively scan all .tsx and .ts files + function scanDirectory(dir) { + const files = fs.readdirSync(dir); + + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + scanDirectory(filePath); + } else if (file.endsWith('.tsx') || file.endsWith('.ts')) { + const content = fs.readFileSync(filePath, 'utf8'); + + // Match LocalIcon usage: + const localIconMatches = content.match(/]*icon="([^"]+)"/g); + if (localIconMatches) { + localIconMatches.forEach(match => { + const iconMatch = match.match(/icon="([^"]+)"/); + if (iconMatch) { + usedIcons.add(iconMatch[1]); + debug(` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); + } + }); + } + + // Match old material-symbols-rounded spans: icon-name + const spanMatches = content.match(/]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g); + if (spanMatches) { + spanMatches.forEach(match => { + const iconMatch = match.match(/>([^<]+)<\/span>/); + if (iconMatch && iconMatch[1].trim()) { + const iconName = iconMatch[1].trim(); + usedIcons.add(iconName); + debug(` Found (legacy): ${iconName} in ${path.relative(srcDir, filePath)}`); + } + }); + } + + // Match Icon component usage: + const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g); + if (iconMatches) { + iconMatches.forEach(match => { + const iconMatch = match.match(/icon="material-symbols:([^"]+)"/); + if (iconMatch) { + usedIcons.add(iconMatch[1]); + debug(` Found (Icon): ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); + } + }); + } + } + }); + } + + scanDirectory(srcDir); + + const iconArray = Array.from(usedIcons).sort(); + info(`📋 Found ${iconArray.length} unique icons across codebase`); + + return iconArray; +} + +// Auto-detect used icons +const usedIcons = scanForUsedIcons(); + +// Check if we need to regenerate (compare with existing) +const outputPath = path.join(__dirname, '..', 'src', 'assets', 'material-symbols-icons.json'); +let needsRegeneration = true; + +if (fs.existsSync(outputPath)) { + try { + const existingSet = JSON.parse(fs.readFileSync(outputPath, 'utf8')); + const existingIcons = Object.keys(existingSet.icons || {}).sort(); + const currentIcons = [...usedIcons].sort(); + + if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) { + needsRegeneration = false; + info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`); + } + } catch (error) { + // If we can't parse existing file, regenerate + needsRegeneration = true; + } +} + +if (!needsRegeneration) { + info('🎉 No regeneration needed!'); + process.exit(0); +} + +info(`🔍 Extracting ${usedIcons.length} icons from Material Symbols...`); + +// Extract only our used icons from the full set +const extractedIcons = getIcons(icons, usedIcons); + +if (!extractedIcons) { + console.error('❌ Failed to extract icons'); + process.exit(1); +} + +// Check for missing icons +const extractedIconNames = Object.keys(extractedIcons.icons || {}); +const missingIcons = usedIcons.filter(icon => !extractedIconNames.includes(icon)); + +if (missingIcons.length > 0) { + info(`⚠️ Missing icons (${missingIcons.length}): ${missingIcons.join(', ')}`); + info('💡 These icons don\'t exist in Material Symbols. Please use available alternatives.'); +} + +// Create output directory +const outputDir = path.join(__dirname, '..', 'src', 'assets'); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +// Write the extracted icon set to a file (outputPath already defined above) +fs.writeFileSync(outputPath, JSON.stringify(extractedIcons, null, 2)); + +info(`✅ Successfully extracted ${Object.keys(extractedIcons.icons || {}).length} icons`); +info(`📦 Bundle size: ${Math.round(JSON.stringify(extractedIcons).length / 1024)}KB`); +info(`💾 Saved to: ${outputPath}`); + +// Generate TypeScript types +const typesContent = `// Auto-generated icon types +// This file is automatically generated by scripts/generate-icons.js +// Do not edit manually - changes will be overwritten + +export type MaterialSymbolIcon = ${usedIcons.map(icon => `'${icon}'`).join(' | ')}; + +export interface IconSet { + prefix: string; + icons: Record; + width?: number; + height?: number; +} + +// Re-export the icon set as the default export with proper typing +declare const iconSet: IconSet; +export default iconSet; +`; + +const typesPath = path.join(outputDir, 'material-symbols-icons.d.ts'); +fs.writeFileSync(typesPath, typesContent); + +info(`📝 Generated types: ${typesPath}`); +info(`🎉 Icon extraction complete!`); \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e628dc4de..ef4d663f6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,24 +1,30 @@ -import React, { Suspense } from 'react'; -import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider'; -import { FileContextProvider } from './contexts/FileContext'; -import { NavigationProvider } from './contexts/NavigationContext'; -import { FilesModalProvider } from './contexts/FilesModalContext'; -import HomePage from './pages/HomePage'; +import React, { Suspense } from "react"; +import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider"; +import { FileContextProvider } from "./contexts/FileContext"; +import { NavigationProvider } from "./contexts/NavigationContext"; +import { FilesModalProvider } from "./contexts/FilesModalContext"; +import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext"; +import { SidebarProvider } from "./contexts/SidebarContext"; +import ErrorBoundary from "./components/shared/ErrorBoundary"; +import HomePage from "./pages/HomePage"; // Import global styles -import './styles/tailwind.css'; -import './index.css'; +import "./styles/tailwind.css"; +import "./index.css"; +import { RightRailProvider } from "./contexts/RightRailContext"; // Loading component for i18next suspense const LoadingFallback = () => ( -
+
Loading...
); @@ -27,13 +33,21 @@ export default function App() { return ( }> - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/frontend/src/assets/3rdPartyLicenses.json b/frontend/src/assets/3rdPartyLicenses.json index 0235380af..2f19f5db6 100644 --- a/frontend/src/assets/3rdPartyLicenses.json +++ b/frontend/src/assets/3rdPartyLicenses.json @@ -21,6 +21,13 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "@atlaskit/pragmatic-drag-and-drop", + "moduleUrl": "https://github.com/atlassian/pragmatic-drag-and-drop", + "moduleVersion": "1.7.4", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "@babel/code-frame", "moduleUrl": "https://github.com/babel/babel", @@ -59,7 +66,7 @@ { "moduleName": "@babel/parser", "moduleUrl": "https://github.com/babel/babel", - "moduleVersion": "7.27.3", + "moduleVersion": "7.28.3", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -87,7 +94,7 @@ { "moduleName": "@babel/types", "moduleUrl": "https://github.com/babel/babel", - "moduleVersion": "7.27.3", + "moduleVersion": "7.28.2", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -217,6 +224,20 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "@iconify/react", + "moduleUrl": "https://github.com/iconify/iconify", + "moduleVersion": "6.0.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, + { + "moduleName": "@iconify/types", + "moduleUrl": "https://github.com/iconify/iconify", + "moduleVersion": "2.0.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "@isaacs/fs-minipass", "moduleUrl": "https://github.com/npm/fs-minipass", @@ -399,6 +420,20 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "@tanstack/react-virtual", + "moduleUrl": "https://github.com/TanStack/virtual", + "moduleVersion": "3.13.12", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, + { + "moduleName": "@tanstack/virtual-core", + "moduleUrl": "https://github.com/TanStack/virtual", + "moduleVersion": "3.13.12", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "@testing-library/dom", "moduleUrl": "https://github.com/testing-library/dom-testing-library", @@ -567,6 +602,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "bind-event-listener", + "moduleUrl": "https://github.com/alexreardon/bind-event-listener", + "moduleVersion": "3.0.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "brace-expansion", "moduleUrl": "https://github.com/juliangruber/brace-expansion", @@ -1246,13 +1288,6 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, - { - "moduleName": "material-symbols", - "moduleUrl": "https://github.com/marella/material-symbols", - "moduleVersion": "0.33.0", - "moduleLicense": "Apache-2.0", - "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" - }, { "moduleName": "math-intrinsics", "moduleUrl": "https://github.com/es-shims/math-intrinsics", @@ -1494,7 +1529,7 @@ { "moduleName": "postcss", "moduleUrl": "https://github.com/postcss/postcss", - "moduleVersion": "8.5.3", + "moduleVersion": "8.5.6", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -1526,6 +1561,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "raf-schd", + "moduleUrl": "https://github.com/alexreardon/raf-schd", + "moduleVersion": "4.0.3", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "react-dom", "moduleUrl": "https://github.com/facebook/react", diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx index 1c327cefa..4294180f3 100644 --- a/frontend/src/components/FileManager.tsx +++ b/frontend/src/components/FileManager.tsx @@ -111,7 +111,7 @@ const FileManager: React.FC = ({ selectedTool }) => { onClose={closeFilesModal} size={isMobile ? "100%" : "auto"} centered - radius={30} + radius="md" className="overflow-hidden p-0" withCloseButton={false} styles={{ @@ -144,7 +144,7 @@ const FileManager: React.FC = ({ selectedTool }) => { height: '100%', width: '100%', border: 'none', - borderRadius: '30px', + borderRadius: 'var(--radius-md)', backgroundColor: 'var(--bg-file-manager)' }} styles={{ diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index c93e78670..df1197ab9 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { - Button, Text, Center, Box, Notification, TextInput, LoadingOverlay, Modal, Alert, Container, - Stack, Group + Text, Center, Box, Notification, LoadingOverlay, Stack, Group, Portal } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useTranslation } from 'react-i18next'; @@ -466,21 +465,6 @@ const FileEditor = ({ - - {toolMode && ( - <> - - - - )} - {showBulkActions && !toolMode && ( - <> - - - )} - {activeFileRecords.length === 0 && !zipExtractionProgress.isExtracting ? ( @@ -573,25 +557,29 @@ const FileEditor = ({ /> {status && ( - setStatus(null)} - style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }} - > - {status} - + + setStatus(null)} + style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 10001 }} + > + {status} + + )} {error && ( - setError(null)} - style={{ position: 'fixed', bottom: 80, right: 20, zIndex: 1000 }} - > - {error} - + + setError(null)} + style={{ position: 'fixed', bottom: 80, right: 20, zIndex: 10001 }} + > + {error} + + )} diff --git a/frontend/src/components/fileManager/FileInfoCard.tsx b/frontend/src/components/fileManager/FileInfoCard.tsx index f8cc84cb8..68c2a491d 100644 --- a/frontend/src/components/fileManager/FileInfoCard.tsx +++ b/frontend/src/components/fileManager/FileInfoCard.tsx @@ -17,7 +17,7 @@ const FileInfoCard: React.FC = ({ return ( - + {t('fileManager.details', 'File Details')} @@ -31,7 +31,7 @@ const FileInfoCard: React.FC = ({ - + {t('fileManager.fileFormat', 'Format')} {currentFile ? ( @@ -43,7 +43,7 @@ const FileInfoCard: React.FC = ({ )} - + {t('fileManager.fileSize', 'Size')} @@ -51,7 +51,7 @@ const FileInfoCard: React.FC = ({ - + {t('fileManager.fileVersion', 'Version')} @@ -64,4 +64,4 @@ const FileInfoCard: React.FC = ({ ); }; -export default FileInfoCard; \ No newline at end of file +export default FileInfoCard; diff --git a/frontend/src/components/fileManager/FileSourceButtons.tsx b/frontend/src/components/fileManager/FileSourceButtons.tsx index a6870a661..d2d28e09e 100644 --- a/frontend/src/components/fileManager/FileSourceButtons.tsx +++ b/frontend/src/components/fileManager/FileSourceButtons.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Stack, Text, Button, Group } from '@mantine/core'; import HistoryIcon from '@mui/icons-material/History'; -import FolderIcon from '@mui/icons-material/Folder'; +import UploadIcon from '@mui/icons-material/Upload'; import CloudIcon from '@mui/icons-material/Cloud'; import { useTranslation } from 'react-i18next'; import { useFileManagerContext } from '../../contexts/FileManagerContext'; @@ -10,7 +10,7 @@ interface FileSourceButtonsProps { horizontal?: boolean; } -const FileSourceButtons: React.FC = ({ +const FileSourceButtons: React.FC = ({ horizontal = false }) => { const { activeSource, onSourceChange, onLocalFileClick } = useFileManagerContext(); @@ -44,11 +44,11 @@ const FileSourceButtons: React.FC = ({ > {horizontal ? t('fileManager.recent', 'Recent') : t('fileManager.recent', 'Recent')} - + - + + + ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/frontend/src/components/shared/FilePreview.tsx b/frontend/src/components/shared/FilePreview.tsx index a13894040..06c55ee2d 100644 --- a/frontend/src/components/shared/FilePreview.tsx +++ b/frontend/src/components/shared/FilePreview.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Box } from '@mantine/core'; +import { Box, Center } from '@mantine/core'; +import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import { FileMetadata } from '../../types/file'; import DocumentThumbnail from './filePreview/DocumentThumbnail'; import DocumentStack from './filePreview/DocumentStack'; @@ -38,7 +39,21 @@ const FilePreview: React.FC = ({ onPrevious, onNext }) => { - if (!file) return null; + if (!file) { + return ( + +
+ +
+
+ ); + } const hasMultipleFiles = totalFiles > 1; diff --git a/frontend/src/components/shared/LandingPage.tsx b/frontend/src/components/shared/LandingPage.tsx index 6c1668a43..14322076e 100644 --- a/frontend/src/components/shared/LandingPage.tsx +++ b/frontend/src/components/shared/LandingPage.tsx @@ -1,21 +1,28 @@ 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'; const LandingPage = () => { const { addMultipleFiles } = useFileHandler(); const fileInputRef = React.useRef(null); const { colorScheme } = useMantineColorScheme(); const { t } = useTranslation(); + const { openFilesModal } = useFilesModalContext(); + const [isUploadHover, setIsUploadHover] = React.useState(false); const handleFileDrop = async (files: File[]) => { await addMultipleFiles(files); }; - const handleAddFilesClick = () => { + const handleOpenFilesModal = () => { + openFilesModal(); + }; + + const handleNativeUploadClick = () => { fileInputRef.current?.click(); }; @@ -44,7 +51,7 @@ const LandingPage = () => { borderRadius: '0.5rem 0.5rem 0 0', filter: 'var(--drop-shadow-filter)', backgroundColor: 'var(--landing-paper-bg)', - transition: 'background-color 0.2s ease', + transition: 'background-color 0.4s ease', }} activateOnClick={false} styles={{ @@ -99,26 +106,73 @@ const LandingPage = () => { />
- {/* Add Files Button */} - + + +
{/* Hidden file input for native file picker */} { +interface LanguageSelectorProps { + position?: React.ComponentProps['position']; + offset?: number; + compact?: boolean; // icon-only trigger +} + +const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = false }: LanguageSelectorProps) => { const { i18n } = useTranslation(); const [opened, setOpened] = useState(false); const [animationTriggered, setAnimationTriggered] = useState(false); @@ -21,26 +27,27 @@ const LanguageSelector = () => { })); const handleLanguageChange = (value: string, event: React.MouseEvent) => { - // Create ripple effect at click position - const rect = event.currentTarget.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - - setRippleEffect({ x, y, key: Date.now() }); - + // Create ripple effect at click position (only for button mode) + if (!compact) { + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + setRippleEffect({ x, y, key: Date.now() }); + } + // Start transition animation setIsChanging(true); setPendingLanguage(value); - + // Simulate processing time for smooth transition setTimeout(() => { i18n.changeLanguage(value); - + setTimeout(() => { setIsChanging(false); setPendingLanguage(null); setOpened(false); - + // Clear ripple effect setTimeout(() => setRippleEffect(null), 100); }, 300); @@ -64,19 +71,9 @@ const LanguageSelector = () => { @@ -84,8 +81,8 @@ const LanguageSelector = () => { opened={opened} onChange={setOpened} width={600} - position="bottom-start" - offset={8} + position={position} + offset={offset} transitionProps={{ transition: 'scale-y', duration: 200, @@ -93,29 +90,45 @@ const LanguageSelector = () => { }} > - + }} + > + + + ) : ( + + )} { }} > {option.label} - - {/* Ripple effect */} - {rippleEffect && pendingLanguage === option.value && ( + {!compact && rippleEffect && pendingLanguage === option.value && (
= ({ icon, ...props }) => { + // Convert our icon naming convention to the local collection format + const iconName = icon.startsWith('material-symbols:') + ? icon + : `material-symbols:${icon}`; + + // Development logging (only in dev mode) + if (process.env.NODE_ENV === 'development') { + const logKey = `icon-${iconName}`; + if (!sessionStorage.getItem(logKey)) { + const source = iconsLoaded ? 'local' : 'CDN'; + console.debug(`🎯 Icon: ${iconName} (${source})`); + sessionStorage.setItem(logKey, 'logged'); + } + } + + // Always render the icon - Iconify will use local if available, CDN if not + return ; +}; + +export default LocalIcon; \ No newline at end of file diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index bc041a923..c57e49c40 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -1,9 +1,7 @@ import React, { useState, useRef, forwardRef, useEffect } from "react"; import { ActionIcon, Stack, Divider } from "@mantine/core"; import { useTranslation } from 'react-i18next'; -import MenuBookIcon from "@mui/icons-material/MenuBookRounded"; -import SettingsIcon from "@mui/icons-material/SettingsRounded"; -import FolderIcon from "@mui/icons-material/FolderRounded"; +import LocalIcon from './LocalIcon'; import { useRainbowThemeContext } from "./RainbowThemeProvider"; import AppConfigModal from './AppConfigModal'; import { useIsOverflowing } from '../../hooks/useIsOverflowing'; @@ -13,9 +11,9 @@ import { ButtonConfig } from '../../types/sidebar'; import './quickAccessBar/QuickAccessBar.css'; import AllToolsNavButton from './AllToolsNavButton'; import ActiveToolButton from "./quickAccessBar/ActiveToolButton"; -import { - isNavButtonActive, - getNavButtonStyle, +import { + isNavButtonActive, + getNavButtonStyle, getActiveNavButton, } from './quickAccessBar/QuickAccessBar'; @@ -39,12 +37,12 @@ const QuickAccessBar = forwardRef(({ openFilesModal(); }; - + const buttonConfigs: ButtonConfig[] = [ { id: 'read', name: t("quickAccess.read", "Read"), - icon: , + icon: , size: 'lg', isRound: false, type: 'navigation', @@ -54,28 +52,23 @@ const QuickAccessBar = forwardRef(({ handleReaderToggle(); } }, - { - id: 'sign', - name: t("quickAccess.sign", "Sign"), - icon: - - signature - , - size: 'lg', - isRound: false, - type: 'navigation', - onClick: () => { - setActiveButton('sign'); - handleToolSelect('sign'); - } - }, + // TODO: Add sign + //{ + // id: 'sign', + // name: t("quickAccess.sign", "Sign"), + // icon: , + // size: 'lg', + // isRound: false, + // type: 'navigation', + // onClick: () => { + // setActiveButton('sign'); + // handleToolSelect('sign'); + // } + //}, { id: 'automate', name: t("quickAccess.automate", "Automate"), - icon: - - automation - , + icon: , size: 'lg', isRound: false, type: 'navigation', @@ -87,28 +80,26 @@ const QuickAccessBar = forwardRef(({ { id: 'files', name: t("quickAccess.files", "Files"), - icon: , + icon: , isRound: true, size: 'lg', type: 'modal', onClick: handleFilesButtonClick }, - { - id: 'activity', - name: t("quickAccess.activity", "Activity"), - icon: - - vital_signs - , - isRound: true, - size: 'lg', - type: 'navigation', - onClick: () => setActiveButton('activity') - }, + //TODO: Activity + //{ + // id: 'activity', + // name: t("quickAccess.activity", "Activity"), + // icon: , + // isRound: true, + // size: 'lg', + // type: 'navigation', + // onClick: () => setActiveButton('activity') + //}, { id: 'config', name: t("quickAccess.config", "Config"), - icon: , + icon: , size: 'lg', type: 'modal', onClick: () => { @@ -179,8 +170,8 @@ const QuickAccessBar = forwardRef(({
- {/* Add divider after Automate button (index 2) */} - {index === 2 && ( + {/* Add divider after Automate button (index 1) and Files button (index 2) */} + {index === 1 && ( (({ ); }); -export default QuickAccessBar; \ No newline at end of file +export default QuickAccessBar; diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx new file mode 100644 index 000000000..eade0a066 --- /dev/null +++ b/frontend/src/components/shared/RightRail.tsx @@ -0,0 +1,377 @@ +import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import { ActionIcon, Divider, Popover } from '@mantine/core'; +import LocalIcon from './LocalIcon'; +import './rightRail/RightRail.css'; +import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; +import { useRightRail } from '../../contexts/RightRailContext'; +import { useFileState, useFileSelection, useFileManagement } from '../../contexts/FileContext'; +import { useNavigationState } from '../../contexts/NavigationContext'; +import { useTranslation } from 'react-i18next'; +import LanguageSelector from '../shared/LanguageSelector'; +import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; +import { Tooltip } from '../shared/Tooltip'; +import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel'; + +export default function RightRail() { + const { t } = useTranslation(); + const { toggleTheme } = useRainbowThemeContext(); + const { buttons, actions } = useRightRail(); + const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]); + + // Access PageEditor functions for page-editor-specific actions + const { pageEditorFunctions } = useToolWorkflow(); + + // CSV input state for page selection + const [csvInput, setCsvInput] = useState(""); + + // Navigation view + const { currentMode: currentView } = useNavigationState(); + + // File state and selection + const { state, selectors } = useFileState(); + const { selectedFiles, selectedFileIds, selectedPageNumbers, setSelectedFiles, setSelectedPages } = useFileSelection(); + const { removeFiles } = useFileManagement(); + + const activeFiles = selectors.getFiles(); + const filesSignature = selectors.getFilesSignature(); + const fileRecords = selectors.getFileRecords(); + + // Compute selection state and total items + const getSelectionState = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + const totalItems = activeFiles.length; + const selectedCount = selectedFileIds.length; + return { totalItems, selectedCount }; + } + + if (currentView === 'pageEditor') { + let totalItems = 0; + fileRecords.forEach(rec => { + const pf = rec.processedFile; + if (pf) { + totalItems += (pf.totalPages as number) || (pf.pages?.length || 0); + } + }); + const selectedCount = Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length : 0; + return { totalItems, selectedCount }; + } + + return { totalItems: 0, selectedCount: 0 }; + }, [currentView, activeFiles, fileRecords, selectedFileIds, selectedPageNumbers]); + + const { totalItems, selectedCount } = getSelectionState(); + + const handleSelectAll = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + // Select all file IDs + const allIds = state.files.ids; + setSelectedFiles(allIds); + return; + } + + if (currentView === 'pageEditor') { + let totalPages = 0; + fileRecords.forEach(rec => { + const pf = rec.processedFile; + if (pf) { + totalPages += (pf.totalPages as number) || (pf.pages?.length || 0); + } + }); + + if (totalPages > 0) { + setSelectedPages(Array.from({ length: totalPages }, (_, i) => i + 1)); + } + } + }, [currentView, state.files.ids, fileRecords, setSelectedFiles, setSelectedPages]); + + const handleDeselectAll = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + setSelectedFiles([]); + return; + } + if (currentView === 'pageEditor') { + setSelectedPages([]); + } + }, [currentView, setSelectedFiles, setSelectedPages]); + + const handleExportAll = useCallback(() => { + if (currentView === 'fileEditor' || currentView === 'viewer') { + // Download selected files (or all if none selected) + const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles; + + filesToDownload.forEach(file => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(file); + link.download = file.name; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); + }); + } else if (currentView === 'pageEditor') { + // Export all pages (not just selected) + pageEditorFunctions?.onExportAll?.(); + } + }, [currentView, activeFiles, selectedFiles, pageEditorFunctions]); + + const handleCloseSelected = useCallback(() => { + if (currentView !== 'fileEditor') return; + if (selectedFileIds.length === 0) return; + + // Close only selected files (do not delete from storage) + removeFiles(selectedFileIds, false); + + // Clear selection after closing + setSelectedFiles([]); + }, [currentView, selectedFileIds, removeFiles, setSelectedFiles]); + + // CSV parsing functions for page selection + const parseCSVInput = useCallback((csv: string) => { + const pageNumbers: number[] = []; + const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); + + ranges.forEach(range => { + if (range.includes('-')) { + const [start, end] = range.split('-').map(n => parseInt(n.trim())); + for (let i = start; i <= end; i++) { + if (i > 0) { + pageNumbers.push(i); + } + } + } else { + const pageNum = parseInt(range); + if (pageNum > 0) { + pageNumbers.push(pageNum); + } + } + }); + + return pageNumbers; + }, []); + + const updatePagesFromCSV = useCallback(() => { + const rawPages = parseCSVInput(csvInput); + // Determine max page count from processed records + const maxPages = fileRecords.reduce((sum, rec) => { + const pf = rec.processedFile; + if (!pf) return sum; + return sum + ((pf.totalPages as number) || (pf.pages?.length || 0)); + }, 0); + const normalized = Array.from(new Set(rawPages.filter(n => Number.isFinite(n) && n > 0 && n <= maxPages))).sort((a,b)=>a-b); + setSelectedPages(normalized); + }, [csvInput, parseCSVInput, fileRecords, setSelectedPages]); + + // Sync csvInput with selectedPageNumbers changes + useEffect(() => { + const sortedPageNumbers = Array.isArray(selectedPageNumbers) + ? [...selectedPageNumbers].sort((a, b) => a - b) + : []; + const newCsvInput = sortedPageNumbers.join(', '); + setCsvInput(newCsvInput); + }, [selectedPageNumbers]); + + // Clear CSV input when files change (use stable signature to avoid ref churn) + useEffect(() => { + setCsvInput(""); + }, [filesSignature]); + + // Mount/visibility for page-editor-only buttons to allow exit animation, then remove to avoid flex gap + const [pageControlsMounted, setPageControlsMounted] = useState(currentView === 'pageEditor'); + const [pageControlsVisible, setPageControlsVisible] = useState(currentView === 'pageEditor'); + + useEffect(() => { + if (currentView === 'pageEditor') { + // Mount and show + setPageControlsMounted(true); + // Next tick to ensure transition applies + requestAnimationFrame(() => setPageControlsVisible(true)); + } else { + // Start exit animation + setPageControlsVisible(false); + // After transition, unmount to remove flex gap + const timer = setTimeout(() => setPageControlsMounted(false), 240); + return () => clearTimeout(timer); + } + }, [currentView]); + + return ( +
+
+ {topButtons.length > 0 && ( + <> +
+ {topButtons.map(btn => ( + + actions[btn.id]?.()} + disabled={btn.disabled} + > + {btn.icon} + + + ))} +
+ + + )} + + {/* Group: Selection controls + Close, animate as one unit when entering/leaving viewer */} +
+
+ {/* Select All Button */} + +
+ + + +
+
+ + {/* Deselect All Button */} + +
+ + + +
+
+ + {/* Select by Numbers - page editor only, with animated presence */} + {pageControlsMounted && ( + + +
+ + +
+ + + +
+
+ +
+ +
+
+
+
+
+ + )} + + {/* Delete Selected Pages - page editor only, with animated presence */} + {pageControlsMounted && ( + + +
+
+ { pageEditorFunctions?.handleDelete?.(); setSelectedPages([]); }} + disabled={!pageControlsVisible || (Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length === 0 : true)} + aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'} + > + + +
+
+
+ + )} + + {/* Close (File Editor: Close Selected | Page Editor: Close PDF) */} + +
+ pageEditorFunctions?.closePdf?.() : handleCloseSelected} + disabled={ + currentView === 'viewer' || + (currentView === 'fileEditor' && selectedCount === 0) || + (currentView === 'pageEditor' && (activeFiles.length === 0 || !pageEditorFunctions?.closePdf)) + } + > + + +
+
+
+ + +
+ + {/* Theme toggle and Language dropdown */} +
+ + + + + + + + + 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All')) + } position="left" offset={12} arrow> +
+ + + +
+
+
+ +
+
+
+ ); +} + + diff --git a/frontend/src/components/shared/TextInput.tsx b/frontend/src/components/shared/TextInput.tsx index e44e8efb2..fc3e99015 100644 --- a/frontend/src/components/shared/TextInput.tsx +++ b/frontend/src/components/shared/TextInput.tsx @@ -1,5 +1,6 @@ import React, { forwardRef } from 'react'; import { useMantineColorScheme } from '@mantine/core'; +import LocalIcon from './LocalIcon'; import styles from './textInput/TextInput.module.css'; /** @@ -96,7 +97,7 @@ export const TextInput = forwardRef(({ style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }} aria-label="Clear input" > - close + )}
diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index c415eddf5..7940112ca 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; +import LocalIcon from './LocalIcon'; import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils'; import { useTooltipPosition } from '../../hooks/useTooltipPosition'; import { TooltipTip } from '../../types/tips'; @@ -124,8 +125,8 @@ export const Tooltip: React.FC = ({ if (sidebarTooltip) return null; switch (position) { - case 'top': return "tooltip-arrow tooltip-arrow-top"; - case 'bottom': return "tooltip-arrow tooltip-arrow-bottom"; + case 'top': return "tooltip-arrow tooltip-arrow-bottom"; + case 'bottom': return "tooltip-arrow tooltip-arrow-top"; case 'left': return "tooltip-arrow tooltip-arrow-left"; case 'right': return "tooltip-arrow tooltip-arrow-right"; default: return "tooltip-arrow tooltip-arrow-right"; @@ -150,7 +151,7 @@ export const Tooltip: React.FC = ({ position: 'fixed', top: coords.top, left: coords.left, - width: (maxWidth !== undefined ? maxWidth : '25rem'), + width: (maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' : undefined)), minWidth: minWidth, zIndex: 9999, visibility: positionReady ? 'visible' : 'hidden', @@ -171,9 +172,7 @@ export const Tooltip: React.FC = ({ }} title="Close tooltip" > - - close - + )} {arrow && getArrowClass() && ( diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index ee5591694..229c3d362 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -1,23 +1,64 @@ -import React, { useState, useCallback, useMemo } from "react"; -import { Button, SegmentedControl, Loader } from "@mantine/core"; +import React, { useState, useCallback } from "react"; +import { SegmentedControl, Loader } from "@mantine/core"; import { useRainbowThemeContext } from "./RainbowThemeProvider"; -import LanguageSelector from "./LanguageSelector"; import rainbowStyles from '../../styles/rainbow.module.css'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LightModeIcon from '@mui/icons-material/LightMode'; -import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; import FolderIcon from "@mui/icons-material/Folder"; -import { Group } from "@mantine/core"; -import { ModeType } from '../../contexts/NavigationContext'; +import { ModeType, isValidMode } from '../../contexts/NavigationContext'; -// Stable view option objects that don't recreate on every render -const VIEW_OPTIONS_BASE = [ - { value: "viewer", icon: VisibilityIcon }, - { value: "pageEditor", icon: EditNoteIcon }, - { value: "fileEditor", icon: FolderIcon }, -] as const; +const viewOptionStyle = { + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', + gap: 6, + whiteSpace: 'nowrap', + paddingTop: '0.3rem', +} + + +// Create view options with icons and loading states +const createViewOptions = (switchingTo: ModeType | null) => [ + { + label: ( +
+ {switchingTo === "viewer" ? ( + + ) : ( + + )} + Read +
+ ), + value: "viewer", + }, + { + label: ( +
+ {switchingTo === "pageEditor" ? ( + + ) : ( + + )} + Page Editor +
+ ), + value: "pageEditor", + }, + { + label: ( +
+ {switchingTo === "fileEditor" ? ( + + ) : ( + + )} + File Manager +
+ ), + value: "fileEditor", + }, +]; interface TopControlsProps { currentView: ModeType; @@ -30,90 +71,60 @@ const TopControls = ({ setCurrentView, selectedToolKey, }: TopControlsProps) => { - const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext(); - const [switchingTo, setSwitchingTo] = useState(null); + const { isRainbowMode } = useRainbowThemeContext(); + const [switchingTo, setSwitchingTo] = useState(null); const isToolSelected = selectedToolKey !== null; const handleViewChange = useCallback((view: string) => { - // Guard against redundant changes - if (view === currentView) return; - + if (!isValidMode(view)) { + // Ignore invalid values defensively + return; + } + const mode = view as ModeType; + // Show immediate feedback - setSwitchingTo(view); + setSwitchingTo(mode as ModeType); // Defer the heavy view change to next frame so spinner can render requestAnimationFrame(() => { // Give the spinner one more frame to show requestAnimationFrame(() => { - setCurrentView(view as ModeType); - + setCurrentView(mode as ModeType); + // Clear the loading state after view change completes setTimeout(() => setSwitchingTo(null), 300); }); }); - }, [setCurrentView, currentView]); - - // Memoize the SegmentedControl data with stable references - const viewOptions = useMemo(() => - VIEW_OPTIONS_BASE.map(option => ({ - value: option.value, - label: ( - - {switchingTo === option.value ? ( - - ) : ( - - )} - - ) - })), [switchingTo]); - - const getThemeIcon = () => { - if (isRainbowMode) return ; - if (themeMode === "dark") return ; - return ; - }; + }, [setCurrentView]); return (
-
- - -
{!isToolSelected && ( -
+
diff --git a/frontend/src/components/shared/rightRail/RightRail.README.md b/frontend/src/components/shared/rightRail/RightRail.README.md new file mode 100644 index 000000000..7506e927c --- /dev/null +++ b/frontend/src/components/shared/rightRail/RightRail.README.md @@ -0,0 +1,108 @@ +# RightRail Component + +A dynamic vertical toolbar on the right side of the application that supports both static buttons (Undo/Redo, Save, Print, Share) and dynamic buttons registered by tools. + +## Structure + +- **Top Section**: Dynamic buttons from tools (empty when none) +- **Middle Section**: Grid, Cut, Undo, Redo +- **Bottom Section**: Save, Print, Share + +## Usage + +### For Tools (Recommended) + +```tsx +import { useRightRailButtons } from '../hooks/useRightRailButtons'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; + +function MyTool() { + const handleAction = useCallback(() => { + // Your action here + }, []); + + useRightRailButtons([ + { + id: 'my-action', + icon: , + tooltip: 'Execute Action', + onClick: handleAction, + }, + ]); + + return
My Tool
; +} +``` + +### Multiple Buttons + +```tsx +useRightRailButtons([ + { + id: 'primary', + icon: , + tooltip: 'Primary Action', + order: 1, + onClick: handlePrimary, + }, + { + id: 'secondary', + icon: , + tooltip: 'Secondary Action', + order: 2, + onClick: handleSecondary, + }, +]); +``` + +### Conditional Buttons + +```tsx +useRightRailButtons([ + // Always show + { + id: 'process', + icon: , + tooltip: 'Process', + disabled: isProcessing, + onClick: handleProcess, + }, + // Only show when condition met + ...(hasResults ? [{ + id: 'export', + icon: , + tooltip: 'Export', + onClick: handleExport, + }] : []), +]); +``` + +## API + +### Button Config + +```typescript +interface RightRailButtonWithAction { + id: string; // Unique identifier + icon: React.ReactNode; // Icon component + tooltip: string; // Hover tooltip + section?: 'top' | 'middle' | 'bottom'; // Section (default: 'top') + order?: number; // Sort order (default: 0) + disabled?: boolean; // Disabled state (default: false) + visible?: boolean; // Visibility (default: true) + onClick: () => void; // Click handler +} +``` + +## Built-in Features + +- **Undo/Redo**: Automatically integrates with Page Editor +- **Theme Support**: Light/dark mode with CSS variables +- **Auto Cleanup**: Buttons unregister when tool unmounts + +## Best Practices + +- Use descriptive IDs: `'compress-optimize'`, `'ocr-process'` +- Choose appropriate Material-UI icons +- Keep tooltips concise: `'Compress PDF'`, `'Process with OCR'` +- Use `useCallback` for click handlers to prevent re-registration diff --git a/frontend/src/components/shared/rightRail/RightRail.css b/frontend/src/components/shared/rightRail/RightRail.css new file mode 100644 index 000000000..8d01052a9 --- /dev/null +++ b/frontend/src/components/shared/rightRail/RightRail.css @@ -0,0 +1,127 @@ +.right-rail { + background-color: var(--right-rail-bg); + width: 3.5rem; + min-width: 3.5rem; + max-width: 3.5rem; + position: relative; + z-index: 10; + display: flex; + flex-direction: column; + height: 100vh; + border-left: 1px solid var(--border-subtle); +} + +.right-rail-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding: 1rem 0.5rem; +} + +.right-rail-section { + background-color: var(--right-rail-foreground); + border-radius: 12px; + padding: 0.5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; +} + +.right-rail-divider { + width: 2.75rem; + border: none; + border-top: 1px solid var(--tool-subcategory-rule-color); + margin: 0.25rem 0; +} + +.right-rail-icon { + color: var(--right-rail-icon); +} + +.right-rail-icon[aria-disabled="true"], +.right-rail-icon[disabled] { + color: var(--right-rail-icon-disabled) !important; + background-color: transparent !important; +} + +.right-rail-spacer { + flex: 1; +} + +/* Animated grow-down slot for buttons (mirrors current-tool-slot behavior) */ +.right-rail-slot { + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 450ms ease-out, opacity 300ms ease-out; +} + +.right-rail-enter { + animation: rightRailGrowDown 450ms ease-out; +} + +.right-rail-exit { + animation: rightRailShrinkUp 450ms ease-out; +} + +.right-rail-slot.visible { + max-height: 18rem; /* increased to fit additional controls + divider */ + opacity: 1; +} + +@keyframes rightRailGrowDown { + 0% { + max-height: 0; + opacity: 0; + } + 100% { + max-height: 18rem; + opacity: 1; + } +} + +@keyframes rightRailShrinkUp { + 0% { + max-height: 18rem; + opacity: 1; + } + 100% { + max-height: 0; + opacity: 0; + } +} + +/* Remove bottom margin from close icon */ +.right-rail-slot .right-rail-icon { + margin-bottom: 0; +} + +/* Inline appear/disappear animation for page-number selector button */ +.right-rail-fade { + transition-property: opacity, transform, max-height, visibility; + transition-duration: 220ms, 220ms, 220ms, 0s; + transition-timing-function: ease, ease, ease, linear; + transition-delay: 0s, 0s, 0s, 0s; + transform-origin: top center; + overflow: hidden; +} + +.right-rail-fade.enter { + opacity: 1; + transform: scale(1); + max-height: 3rem; + visibility: visible; +} + +.right-rail-fade.exit { + opacity: 0; + transform: scale(0.85); + max-height: 0; + visibility: hidden; + /* delay visibility change so opacity/max-height can finish */ + transition-delay: 0s, 0s, 0s, 220ms; + pointer-events: none; +} + diff --git a/frontend/src/components/shared/tooltip/Tooltip.module.css b/frontend/src/components/shared/tooltip/Tooltip.module.css index 46902c04b..50c242812 100644 --- a/frontend/src/components/shared/tooltip/Tooltip.module.css +++ b/frontend/src/components/shared/tooltip/Tooltip.module.css @@ -160,7 +160,7 @@ .tooltip-arrow-top { top: -0.25rem; left: 50%; - transform: translateX(-50%) rotate(45deg); + transform: translateX(-50%) rotate(-135deg); border-top: none; border-left: none; } diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx index 949bd2f64..6ecb64b7e 100644 --- a/frontend/src/components/tools/SearchResults.tsx +++ b/frontend/src/components/tools/SearchResults.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { Box, Stack, Text } from '@mantine/core'; -import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; +import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy'; import ToolButton from './toolPicker/ToolButton'; import { useTranslation } from 'react-i18next'; import { useToolSections } from '../../hooks/useToolSections'; @@ -23,8 +23,8 @@ const SearchResults: React.FC = ({ filteredTools, onSelect } return ( {searchGroups.map(group => ( - - + + {group.tools.map(({ id, tool }) => ( void, - showSubcategoryHeader: boolean = true -) => ( - - {showSubcategoryHeader && ( - - )} - - {subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => ( - - ))} - - -); - const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => { const { t } = useTranslation(); const [quickHeaderHeight, setQuickHeaderHeight] = useState(0); @@ -51,29 +25,49 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa const quickAccessRef = useRef(null); const allToolsRef = useRef(null); - // On resize adjust headers height to offset height + // Keep header heights in sync with any dynamic size changes useLayoutEffect(() => { const update = () => { if (quickHeaderRef.current) { - setQuickHeaderHeight(quickHeaderRef.current.offsetHeight); + setQuickHeaderHeight(quickHeaderRef.current.offsetHeight || 0); } if (allHeaderRef.current) { - setAllHeaderHeight(allHeaderRef.current.offsetHeight); + setAllHeaderHeight(allHeaderRef.current.offsetHeight || 0); } }; + update(); + + // Update on window resize window.addEventListener("resize", update); - return () => window.removeEventListener("resize", update); + + // Update on element resize (e.g., font load, badge count change, zoom) + const observers: ResizeObserver[] = []; + if (typeof ResizeObserver !== "undefined") { + const observe = (el: HTMLDivElement | null, cb: () => void) => { + if (!el) return; + const ro = new ResizeObserver(() => cb()); + ro.observe(el); + observers.push(ro); + }; + observe(quickHeaderRef.current, update); + observe(allHeaderRef.current, update); + } + + return () => { + window.removeEventListener("resize", update); + observers.forEach(o => o.disconnect()); + }; }, []); const { sections: visibleSections } = useToolSections(filteredTools); const quickSection = useMemo( - () => visibleSections.find(s => (s as any).key === 'quick'), + () => visibleSections.find(s => s.key === 'quick'), [visibleSections] ); const allSection = useMemo( - () => visibleSections.find(s => (s as any).key === 'all'), + () => visibleSections.find(s => s.key === 'all'), [visibleSections] ); @@ -111,7 +105,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa overflowY: "auto", overflowX: "hidden", minHeight: 0, - height: "100%" + height: "100%", + marginTop: -2 }} className="tool-picker-scrollable" > @@ -120,7 +115,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa {searchGroups.length === 0 ? ( ) : ( - searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect)) + searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect)) )} ) : ( @@ -135,7 +130,6 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa zIndex: 2, borderTop: `0.0625rem solid var(--tool-header-border)`, borderBottom: `0.0625rem solid var(--tool-header-border)`, - marginBottom: -1, padding: "0.5rem 1rem", fontWeight: 700, background: "var(--tool-header-bg)", @@ -143,7 +137,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa cursor: "pointer", display: "flex", alignItems: "center", - justifyContent: "space-between" + justifyContent: "space-between", }} onClick={() => scrollTo(quickAccessRef)} > @@ -164,8 +158,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {quickSection?.subcategories.map(sc => - renderToolButtons(sc, selectedToolKey, onSelect, false) + {quickSection?.subcategories.map(sc => + renderToolButtons(t, sc, selectedToolKey, onSelect, false) )} @@ -178,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)`, @@ -210,8 +204,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {allSection?.subcategories.map(sc => - renderToolButtons(sc, selectedToolKey, onSelect, true) + {allSection?.subcategories.map(sc => + renderToolButtons(t, sc, selectedToolKey, onSelect, true) )} diff --git a/frontend/src/components/tools/addWatermark/AddWatermarkSingleStepSettings.tsx b/frontend/src/components/tools/addWatermark/AddWatermarkSingleStepSettings.tsx new file mode 100644 index 000000000..59ed48e95 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/AddWatermarkSingleStepSettings.tsx @@ -0,0 +1,70 @@ +/** + * AddWatermarkSingleStepSettings - Used for automation only + * + * This component combines all watermark settings into a single step interface + * for use in the automation system. It includes type selection and all relevant + * settings in one unified component. + */ + +import React from "react"; +import { Stack } from "@mantine/core"; +import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; +import WatermarkTypeSettings from "./WatermarkTypeSettings"; +import WatermarkWording from "./WatermarkWording"; +import WatermarkTextStyle from "./WatermarkTextStyle"; +import WatermarkImageFile from "./WatermarkImageFile"; +import WatermarkFormatting from "./WatermarkFormatting"; + +interface AddWatermarkSingleStepSettingsProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; + disabled?: boolean; +} + +const AddWatermarkSingleStepSettings = ({ parameters, onParameterChange, disabled = false }: AddWatermarkSingleStepSettingsProps) => { + return ( + + {/* Watermark Type Selection */} + onParameterChange("watermarkType", type)} + disabled={disabled} + /> + + {/* Conditional settings based on watermark type */} + {parameters.watermarkType === "text" && ( + <> + + + + )} + + {parameters.watermarkType === "image" && ( + + )} + + {/* Formatting settings for both text and image */} + {parameters.watermarkType && ( + + )} + + ); +}; + +export default AddWatermarkSingleStepSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx b/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx index 9a267f638..b6af3365c 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx @@ -6,7 +6,7 @@ import NumberInputWithUnit from "../shared/NumberInputWithUnit"; interface WatermarkFormattingProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx b/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx index 6f38ae206..85e723ccb 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx @@ -6,7 +6,7 @@ import FileUploadButton from "../../shared/FileUploadButton"; interface WatermarkImageFileProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } @@ -17,7 +17,7 @@ const WatermarkImageFile = ({ parameters, onParameterChange, disabled = false }: onParameterChange('watermarkImage', file)} + onChange={(file) => onParameterChange('watermarkImage', file || undefined)} accept="image/*" disabled={disabled} placeholder={t('watermark.settings.image.choose', 'Choose Image')} diff --git a/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx index 2de9335b0..f3c6751cf 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx @@ -5,7 +5,7 @@ import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAdd interface WatermarkStyleSettingsProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } @@ -19,7 +19,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.rotation', 'Rotation (degrees)')} onParameterChange('rotation', value || 0)} + onChange={(value) => onParameterChange('rotation', typeof value === 'number' ? value : (parseInt(value as string, 10) || 0))} min={-360} max={360} disabled={disabled} @@ -28,7 +28,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.opacity', 'Opacity (%)')} onParameterChange('opacity', value || 50)} + onChange={(value) => onParameterChange('opacity', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))} min={0} max={100} disabled={disabled} @@ -40,7 +40,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.spacing.width', 'Width Spacing')} onParameterChange('widthSpacer', value || 50)} + onChange={(value) => onParameterChange('widthSpacer', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))} min={0} max={200} disabled={disabled} @@ -49,7 +49,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals {t('watermark.settings.spacing.height', 'Height Spacing')} onParameterChange('heightSpacer', value || 50)} + onChange={(value) => onParameterChange('heightSpacer', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))} min={0} max={200} disabled={disabled} diff --git a/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx b/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx index 00fd21d09..91217f76b 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx @@ -6,7 +6,7 @@ import { alphabetOptions } from "../../../constants/addWatermarkConstants"; interface WatermarkTextStyleProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/addWatermark/WatermarkWording.tsx b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx index 621a0f399..5278ca332 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkWording.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx @@ -6,7 +6,7 @@ import { removeEmojis } from "../../../utils/textUtils"; interface WatermarkWordingProps { parameters: AddWatermarkParameters; - onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + onParameterChange: (key: K, value: AddWatermarkParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx new file mode 100644 index 000000000..49b12c396 --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -0,0 +1,199 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + Text, + Stack, + Group, + TextInput, + Divider, + Modal +} from '@mantine/core'; +import CheckIcon from '@mui/icons-material/Check'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import ToolConfigurationModal from './ToolConfigurationModal'; +import ToolList from './ToolList'; +import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation'; +import { useAutomationForm } from '../../../hooks/tools/automate/useAutomationForm'; + + +interface AutomationCreationProps { + mode: AutomationMode; + existingAutomation?: AutomationConfig; + onBack: () => void; + onComplete: (automation: AutomationConfig) => void; + toolRegistry: Record; +} + +export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) { + const { t } = useTranslation(); + + const { + automationName, + setAutomationName, + selectedTools, + addTool, + removeTool, + updateTool, + hasUnsavedChanges, + canSaveAutomation, + getToolName, + getToolDefaultParameters + } = useAutomationForm({ mode, existingAutomation, toolRegistry }); + + const [configModalOpen, setConfigModalOpen] = useState(false); + const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1); + const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false); + + + const configureTool = (index: number) => { + setConfiguringToolIndex(index); + setConfigModalOpen(true); + }; + + const handleToolConfigSave = (parameters: Record) => { + if (configuraingToolIndex >= 0) { + updateTool(configuraingToolIndex, { + configured: true, + parameters + }); + } + setConfigModalOpen(false); + setConfiguringToolIndex(-1); + }; + + const handleToolConfigCancel = () => { + setConfigModalOpen(false); + setConfiguringToolIndex(-1); + }; + + const handleToolAdd = () => { + const newTool: AutomationTool = { + id: `tool-${Date.now()}`, + operation: '', + name: t('automate.creation.tools.selectTool', 'Select a tool...'), + configured: false, + parameters: {} + }; + updateTool(selectedTools.length, newTool); + }; + + const handleBackClick = () => { + if (hasUnsavedChanges()) { + setUnsavedWarningOpen(true); + } else { + onBack(); + } + }; + + const handleConfirmBack = () => { + setUnsavedWarningOpen(false); + onBack(); + }; + + const handleCancelBack = () => { + setUnsavedWarningOpen(false); + }; + + const saveAutomation = async () => { + if (!canSaveAutomation()) return; + + const automation = { + name: automationName.trim(), + description: '', + operations: selectedTools.map(tool => ({ + operation: tool.operation, + parameters: tool.parameters || {} + })) + }; + + try { + const { automationStorage } = await import('../../../services/automationStorage'); + const savedAutomation = await automationStorage.saveAutomation(automation); + onComplete(savedAutomation); + } catch (error) { + console.error('Error saving automation:', error); + } + }; + + const currentConfigTool = configuraingToolIndex >= 0 ? selectedTools[configuraingToolIndex] : null; + + return ( +
+ + {t("automate.creation.description", "Automations run tools sequentially. To get started, add tools in the order you want them to run.")} + + + + + {/* Automation Name */} + setAutomationName(e.currentTarget.value)} + size="sm" + /> + + + {/* Selected Tools List */} + {selectedTools.length > 0 && ( + + )} + + + + {/* Save Button */} + + + + {/* Tool Configuration Modal */} + {currentConfigTool && ( + + )} + + {/* Unsaved Changes Warning Modal */} + + + + {t('automate.creation.unsavedChanges.message', 'You have unsaved changes. Are you sure you want to go back? All changes will be lost.')} + + + + + + + +
+ ); +} diff --git a/frontend/src/components/tools/automate/AutomationEntry.tsx b/frontend/src/components/tools/automate/AutomationEntry.tsx new file mode 100644 index 000000000..3314831be --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationEntry.tsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; + +interface AutomationEntryProps { + /** Optional title for the automation (usually for custom ones) */ + title?: string; + /** MUI Icon component for the badge */ + badgeIcon?: React.ComponentType; + /** Array of tool operation names in the workflow */ + operations: string[]; + /** Click handler */ + onClick: () => void; + /** Whether to keep the icon at normal color (for special cases like "Add New") */ + keepIconColor?: boolean; + /** Show menu for saved/suggested automations */ + showMenu?: boolean; + /** Edit handler */ + onEdit?: () => void; + /** Delete handler */ + onDelete?: () => void; +} + +export default function AutomationEntry({ + title, + badgeIcon: BadgeIcon, + operations, + onClick, + keepIconColor = false, + showMenu = false, + onEdit, + onDelete +}: AutomationEntryProps) { + const { t } = useTranslation(); + const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + // Keep item in hovered state if menu is open + const shouldShowHovered = isHovered || isMenuOpen; + + const renderContent = () => { + if (title) { + // Custom automation with title + return ( + + {BadgeIcon && ( + + )} + + {title} + + + ); + } else { + // Suggested automation showing tool chain + return ( + + {BadgeIcon && ( + + )} + + {operations.map((op, index) => ( + + + {t(`${op}.title`, op)} + + + {index < operations.length - 1 && ( + + → + + )} + + ))} + + + ); + } + }; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + +
+ {renderContent()} +
+ + {showMenu && ( + setIsMenuOpen(true)} + onClose={() => setIsMenuOpen(false)} + > + + e.stopPropagation()} + style={{ + opacity: shouldShowHovered ? 1 : 0, + transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)', + transition: 'opacity 0.3s ease, transform 0.3s ease', + pointerEvents: shouldShowHovered ? 'auto' : 'none' + }} + > + + + + + + {onEdit && ( + } + onClick={(e) => { + e.stopPropagation(); + onEdit(); + }} + > + {t('edit', 'Edit')} + + )} + {onDelete && ( + } + onClick={(e) => { + e.stopPropagation(); + onDelete(); + }} + > + {t('delete', 'Delete')} + + )} + + + )} +
+
+ ); +} diff --git a/frontend/src/components/tools/automate/AutomationRun.tsx b/frontend/src/components/tools/automate/AutomationRun.tsx new file mode 100644 index 000000000..640f802f6 --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationRun.tsx @@ -0,0 +1,223 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import CheckIcon from "@mui/icons-material/Check"; +import { useFileSelection } from "../../../contexts/FileContext"; +import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry"; +import { AutomationConfig, ExecutionStep } from "../../../types/automation"; +import { AUTOMATION_CONSTANTS, EXECUTION_STATUS } from "../../../constants/automation"; +import { useResourceCleanup } from "../../../utils/resourceManager"; + +interface AutomationRunProps { + automation: AutomationConfig; + onComplete: () => void; + automateOperation?: any; // TODO: Type this properly when available +} + +export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) { + const { t } = useTranslation(); + const { selectedFiles } = useFileSelection(); + const toolRegistry = useFlatToolRegistry(); + const cleanup = useResourceCleanup(); + + // Progress tracking state + const [executionSteps, setExecutionSteps] = useState([]); + const [currentStepIndex, setCurrentStepIndex] = useState(-1); + + // Use the operation hook's loading state + const isExecuting = automateOperation?.isLoading || false; + const hasResults = automateOperation?.files.length > 0 || automateOperation?.downloadUrl !== null; + + // Initialize execution steps from automation + React.useEffect(() => { + if (automation?.operations) { + const steps = automation.operations.map((op: any, index: number) => { + const tool = toolRegistry[op.operation]; + return { + id: `${op.operation}-${index}`, + operation: op.operation, + name: tool?.name || op.operation, + status: EXECUTION_STATUS.PENDING + }; + }); + setExecutionSteps(steps); + setCurrentStepIndex(-1); + } + }, [automation, toolRegistry]); + + // Cleanup when component unmounts + React.useEffect(() => { + return () => { + // Reset progress state when component unmounts + setExecutionSteps([]); + setCurrentStepIndex(-1); + // Clean up any blob URLs + cleanup(); + }; + }, [cleanup]); + + const executeAutomation = async () => { + if (!selectedFiles || selectedFiles.length === 0) { + return; + } + + if (!automateOperation) { + console.error('No automateOperation provided'); + return; + } + + // Reset progress tracking + setCurrentStepIndex(0); + setExecutionSteps(prev => prev.map(step => ({ ...step, status: EXECUTION_STATUS.PENDING, error: undefined }))); + + try { + // Use the automateOperation.executeOperation to handle file consumption properly + await automateOperation.executeOperation( + { + automationConfig: automation, + onStepStart: (stepIndex: number, operationName: string) => { + setCurrentStepIndex(stepIndex); + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: EXECUTION_STATUS.RUNNING } : step + )); + }, + onStepComplete: (stepIndex: number, resultFiles: File[]) => { + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: EXECUTION_STATUS.COMPLETED } : step + )); + }, + onStepError: (stepIndex: number, error: string) => { + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: EXECUTION_STATUS.ERROR, error } : step + )); + } + }, + selectedFiles + ); + + // Mark all as completed and reset current step + setCurrentStepIndex(-1); + console.log(`✅ Automation completed successfully`); + } catch (error: any) { + console.error("Automation execution failed:", error); + setCurrentStepIndex(-1); + } + }; + + const getProgress = () => { + if (executionSteps.length === 0) return 0; + const completedSteps = executionSteps.filter(step => step.status === EXECUTION_STATUS.COMPLETED).length; + return (completedSteps / executionSteps.length) * 100; + }; + + const getStepIcon = (step: ExecutionStep) => { + switch (step.status) { + case EXECUTION_STATUS.COMPLETED: + return ; + case EXECUTION_STATUS.ERROR: + return ; + case EXECUTION_STATUS.RUNNING: + return
; + default: + return
; + } + }; + + return ( +
+ + {/* Automation Info */} + + + {automation?.name || t("automate.sequence.unnamed", "Unnamed Automation")} + + + {t("automate.sequence.steps", "{{count}} steps", { count: executionSteps.length })} + + + + {/* Progress Bar */} + {isExecuting && ( +
+ + Progress: {currentStepIndex + 1}/{executionSteps.length} + + +
+ )} + + {/* Execution Steps */} + + {executionSteps.map((step, index) => ( + + + {index + 1} + + + {getStepIcon(step)} + +
+ + {step.name} + + {step.error && ( + + {step.error} + + )} +
+
+ ))} +
+ + {/* Action Buttons */} + + + + {hasResults && ( + + )} + +
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/automate/AutomationSelection.tsx b/frontend/src/components/tools/automate/AutomationSelection.tsx new file mode 100644 index 000000000..f55cf4c5d --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationSelection.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Title, Stack, Divider } from "@mantine/core"; +import AddCircleOutline from "@mui/icons-material/AddCircleOutline"; +import SettingsIcon from "@mui/icons-material/Settings"; +import AutomationEntry from "./AutomationEntry"; +import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations"; +import { AutomationConfig } from "../../../types/automation"; + +interface AutomationSelectionProps { + savedAutomations: AutomationConfig[]; + onCreateNew: () => void; + onRun: (automation: AutomationConfig) => void; + onEdit: (automation: AutomationConfig) => void; + onDelete: (automation: AutomationConfig) => void; +} + +export default function AutomationSelection({ + savedAutomations, + onCreateNew, + onRun, + onEdit, + onDelete +}: AutomationSelectionProps) { + const { t } = useTranslation(); + const suggestedAutomations = useSuggestedAutomations(); + + return ( +
+ + {t("automate.selection.saved.title", "Saved")} + + + + + {/* Saved Automations */} + {savedAutomations.map((automation) => ( + typeof op === 'string' ? op : op.operation)} + onClick={() => onRun(automation)} + showMenu={true} + onEdit={() => onEdit(automation)} + onDelete={() => onDelete(automation)} + /> + ))} + + + {/* Suggested Automations */} +
+ + {t("automate.selection.suggested.title", "Suggested")} + + + {suggestedAutomations.map((automation) => ( + op.operation)} + onClick={() => onRun(automation)} + /> + ))} + +
+
+
+ ); +} diff --git a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx new file mode 100644 index 000000000..d97819fb8 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx @@ -0,0 +1,138 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Modal, + Title, + Button, + Group, + Stack, + Text, + Alert +} from '@mantine/core'; +import SettingsIcon from '@mui/icons-material/Settings'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import WarningIcon from '@mui/icons-material/Warning'; +import { ToolRegistry } from '../../../data/toolsTaxonomy'; +import { getAvailableToExtensions } from '../../../utils/convertUtils'; +interface ToolConfigurationModalProps { + opened: boolean; + tool: { + id: string; + operation: string; + name: string; + parameters?: any; + }; + onSave: (parameters: any) => void; + onCancel: () => void; + toolRegistry: ToolRegistry; +} + +export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) { + const { t } = useTranslation(); + + const [parameters, setParameters] = useState({}); + const [isValid, setIsValid] = useState(true); + + // Get tool info from registry + const toolInfo = toolRegistry[tool.operation]; + const SettingsComponent = toolInfo?.settingsComponent; + + // Initialize parameters from tool (which should contain defaults from registry) + useEffect(() => { + if (tool.parameters) { + setParameters(tool.parameters); + } else { + // Fallback to empty parameters if none provided + setParameters({}); + } + }, [tool.parameters, tool.operation]); + + // Render the settings component + const renderToolSettings = () => { + if (!SettingsComponent) { + return ( + } color="orange"> + + {t('automate.config.noSettings', 'This tool does not have configurable settings.')} + + + ); + } + + // Special handling for ConvertSettings which needs additional props + if (tool.operation === 'convert') { + return ( + { + setParameters((prev: any) => ({ ...prev, [key]: value })); + }} + getAvailableToExtensions={getAvailableToExtensions} + selectedFiles={[]} + disabled={false} + /> + ); + } + + return ( + { + setParameters((prev: any) => ({ ...prev, [key]: value })); + }} + disabled={false} + /> + ); + }; + + const handleSave = () => { + if (isValid) { + onSave(parameters); + } + }; + + return ( + + + + {t('automate.config.title', 'Configure {{toolName}}', { toolName: tool.name })} + + + } + size="lg" + centered + > + + + {t('automate.config.description', 'Configure the settings for this tool. These settings will be applied when the automation runs.')} + + +
+ {renderToolSettings()} +
+ + + + + +
+
+ ); +} diff --git a/frontend/src/components/tools/automate/ToolList.tsx b/frontend/src/components/tools/automate/ToolList.tsx new file mode 100644 index 000000000..8b24b5c17 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolList.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Text, Stack, Group, ActionIcon } from '@mantine/core'; +import DeleteIcon from '@mui/icons-material/Delete'; +import SettingsIcon from '@mui/icons-material/Settings'; +import CloseIcon from '@mui/icons-material/Close'; +import AddCircleOutline from '@mui/icons-material/AddCircleOutline'; +import { AutomationTool } from '../../../types/automation'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import ToolSelector from './ToolSelector'; +import AutomationEntry from './AutomationEntry'; + +interface ToolListProps { + tools: AutomationTool[]; + toolRegistry: Record; + onToolUpdate: (index: number, updates: Partial) => void; + onToolRemove: (index: number) => void; + onToolConfigure: (index: number) => void; + onToolAdd: () => void; + getToolName: (operation: string) => string; + getToolDefaultParameters: (operation: string) => Record; +} + +export default function ToolList({ + tools, + toolRegistry, + onToolUpdate, + onToolRemove, + onToolConfigure, + onToolAdd, + getToolName, + getToolDefaultParameters +}: ToolListProps) { + const { t } = useTranslation(); + + const handleToolSelect = (index: number, newOperation: string) => { + const defaultParams = getToolDefaultParameters(newOperation); + + onToolUpdate(index, { + operation: newOperation, + name: getToolName(newOperation), + configured: false, + parameters: defaultParams + }); + }; + + return ( +
+ + {t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length}) + + + {tools.map((tool, index) => ( + +
+ {/* Delete X in top right */} + onToolRemove(index)} + title={t('automate.creation.tools.remove', 'Remove tool')} + style={{ + position: 'absolute', + top: '4px', + right: '4px', + zIndex: 1, + color: 'var(--mantine-color-gray-6)' + }} + > + + + +
+ {/* Tool Selection Dropdown with inline settings cog */} + +
+ handleToolSelect(index, newOperation)} + excludeTools={['automate']} + toolRegistry={toolRegistry} + selectedValue={tool.operation} + placeholder={tool.name} + /> +
+ + {/* Settings cog - only show if tool is selected, aligned right */} + {tool.operation && ( + onToolConfigure(index)} + title={t('automate.creation.tools.configure', 'Configure tool')} + style={{ color: 'var(--mantine-color-gray-6)' }} + > + + + )} +
+ + {/* Configuration status underneath */} + {tool.operation && !tool.configured && ( + + {t('automate.creation.tools.notConfigured', "! Not Configured")} + + )} +
+
+ + {index < tools.length - 1 && ( +
+ +
+ )} +
+ ))} + + {/* Arrow before Add Tool Button */} + {tools.length > 0 && ( +
+ +
+ )} + + {/* Add Tool Button */} +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx new file mode 100644 index 000000000..80b68b0a4 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -0,0 +1,182 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Menu, Stack, Text, ScrollArea } from '@mantine/core'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import { useToolSections } from '../../../hooks/useToolSections'; +import { renderToolButtons } from '../shared/renderToolButtons'; +import ToolSearch from '../toolPicker/ToolSearch'; + +interface ToolSelectorProps { + onSelect: (toolKey: string) => void; + excludeTools?: string[]; + toolRegistry: Record; // Pass registry as prop to break circular dependency + selectedValue?: string; // For showing current selection when editing existing tool + placeholder?: string; // Custom placeholder text +} + +export default function ToolSelector({ + onSelect, + excludeTools = [], + toolRegistry, + selectedValue, + placeholder +}: ToolSelectorProps) { + const { t } = useTranslation(); + const [opened, setOpened] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + + // Filter out excluded tools (like 'automate' itself) + const baseFilteredTools = useMemo(() => { + return Object.entries(toolRegistry).filter(([key]) => !excludeTools.includes(key)); + }, [toolRegistry, excludeTools]); + + // Apply search filter + const filteredTools = useMemo(() => { + if (!searchTerm.trim()) { + return baseFilteredTools; + } + + const lowercaseSearch = searchTerm.toLowerCase(); + return baseFilteredTools.filter(([key, tool]) => { + return ( + tool.name.toLowerCase().includes(lowercaseSearch) || + tool.description?.toLowerCase().includes(lowercaseSearch) || + key.toLowerCase().includes(lowercaseSearch) + ); + }); + }, [baseFilteredTools, searchTerm]); + + // Create filtered tool registry for ToolSearch + const filteredToolRegistry = useMemo(() => { + const registry: Record = {}; + baseFilteredTools.forEach(([key, tool]) => { + registry[key] = tool; + }); + return registry; + }, [baseFilteredTools]); + + // Use the same tool sections logic as the main ToolPicker + const { sections, searchGroups } = useToolSections(filteredTools); + + // Determine what to display: search results or organized sections + const isSearching = searchTerm.trim().length > 0; + const displayGroups = useMemo(() => { + if (isSearching) { + return searchGroups || []; + } + + if (!sections || sections.length === 0) { + return []; + } + + // Find the "all" section which contains all tools without duplicates + const allSection = sections.find(s => (s as any).key === 'all'); + return allSection?.subcategories || []; + }, [isSearching, searchGroups, sections]); + + const handleToolSelect = useCallback((toolKey: string) => { + onSelect(toolKey); + setOpened(false); + setSearchTerm(''); // Clear search to show the selected tool display + }, [onSelect]); + + const renderedTools = useMemo(() => + displayGroups.map((subcategory) => + renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching) + ), [displayGroups, handleToolSelect, isSearching, t] + ); + + const handleSearchFocus = () => { + setOpened(true); + }; + + const handleSearchChange = (value: string) => { + setSearchTerm(value); + if (!opened) { + setOpened(true); + } + }; + + // Get display value for selected tool + const getDisplayValue = () => { + if (selectedValue && toolRegistry[selectedValue]) { + return toolRegistry[selectedValue].name; + } + return placeholder || t('automate.creation.tools.add', 'Add a tool...'); + }; + + return ( +
+ { + setOpened(isOpen); + // Clear search term when menu closes to show proper display + if (!isOpen) { + setSearchTerm(''); + } + }} + closeOnClickOutside={true} + closeOnEscape={true} + position="bottom-start" + offset={4} + withinPortal={false} + trapFocus={false} + shadow="sm" + transitionProps={{ duration: 0 }} + > + +
+ {selectedValue && toolRegistry[selectedValue] && !opened ? ( + // Show selected tool in AutomationEntry style when tool is selected and not searching +
+
+
+ {toolRegistry[selectedValue].icon} +
+ + {toolRegistry[selectedValue].name} + +
+
+ ) : ( + // Show search input when no tool selected or actively searching + + )} +
+
+ + + + + {displayGroups.length === 0 ? ( + + {isSearching + ? t('tools.noSearchResults', 'No tools found') + : t('tools.noTools', 'No tools available') + } + + ) : ( + renderedTools + )} + + + +
+
+ ); +} diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx index 22a25e627..20e7c4d7a 100644 --- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx +++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx @@ -1,6 +1,9 @@ import React from "react"; -import { Text } from "@mantine/core"; +import { Text, Anchor } from "@mantine/core"; import { useTranslation } from "react-i18next"; +import FolderIcon from '@mui/icons-material/Folder'; +import { useFilesModalContext } from "../../../contexts/FilesModalContext"; +import { useAllFiles } from "../../../contexts/FileContext"; export interface FileStatusIndicatorProps { selectedFiles?: File[]; @@ -12,13 +15,39 @@ const FileStatusIndicator = ({ placeholder, }: FileStatusIndicatorProps) => { const { t } = useTranslation(); - const defaultPlaceholder = placeholder || t("files.placeholder", "Select a PDF file in the main view to get started"); - - // Only show content when no files are selected + const { openFilesModal } = useFilesModalContext(); + const { files: workbenchFiles } = useAllFiles(); + + // Check if there are no files in the workbench + if (workbenchFiles.length === 0) { + return ( + + {t("files.noFiles", "No files uploaded. ")}{" "} + + + {t("files.addFiles", "Add files")} + + + ); + } + + // Show selection status when there are files in workbench if (selectedFiles.length === 0) { return ( - {defaultPlaceholder} + {t("files.selectFromWorkbench", "Select files from the workbench or ") + " "} + + + {t("files.addFiles", "Add files")} + ); } diff --git a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx index b1f91fc39..fca3b5e56 100644 --- a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx +++ b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Stack, Text, Divider, Card, Group } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useSuggestedTools } from '../../../hooks/useSuggestedTools'; + export interface SuggestedToolsSectionProps {} export function SuggestedToolsSection(): React.ReactElement { @@ -21,7 +22,7 @@ export function SuggestedToolsSection(): React.ReactElement { const IconComponent = tool.icon; return ( { - if (tooltip && !isCollapsed) { + if (tooltip && (!isCollapsed || alwaysShowTooltip)) { return ( {title} - - gpp_maybe - + ); @@ -77,6 +76,7 @@ const ToolStep = ({ showNumber, _stepNumber, _noPadding, + alwaysShowTooltip = false, tooltip }: ToolStepProps) => { if (!isVisible) return null; @@ -118,18 +118,16 @@ const ToolStep = ({ {stepNumber} )} - {renderTooltipTitle(title, tooltip, isCollapsed)} + {renderTooltipTitle(title, tooltip, isCollapsed, alwaysShowTooltip)} {isCollapsed ? ( - ) : ( - diff --git a/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx new file mode 100644 index 000000000..6ed949442 --- /dev/null +++ b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Flex, Text, Divider } from '@mantine/core'; +import LocalIcon from '../../shared/LocalIcon'; +import { Tooltip } from '../../shared/Tooltip'; + +export interface ToolWorkflowTitleProps { + title: string; + tooltip?: { + content?: React.ReactNode; + tips?: any[]; + header?: { + title: string; + logo?: React.ReactNode; + }; + }; +} + +export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) { + if (tooltip) { + return ( + <> + + + e.stopPropagation()}> + + {title} + + + + + + + + ); + } + + return ( + <> + + + {title} + + + + + ); +} diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index 3cb46ed60..d523ff6f6 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -3,12 +3,14 @@ import { Stack } from '@mantine/core'; import { createToolSteps, ToolStepProvider } from './ToolStep'; import OperationButton from './OperationButton'; import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation'; +import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle'; export interface FilesStepConfig { selectedFiles: File[]; isCollapsed?: boolean; placeholder?: string; onCollapsedClick?: () => void; + isVisible?: boolean; } export interface MiddleStepConfig { @@ -44,7 +46,10 @@ export interface ReviewStepConfig { testId?: string; } +export interface TitleConfig extends ToolWorkflowTitleProps {} + export interface ToolFlowConfig { + title?: TitleConfig; files: FilesStepConfig; steps: MiddleStepConfig[]; executeButton?: ExecuteButtonConfig; @@ -62,8 +67,10 @@ export function createToolFlow(config: ToolFlowConfig) { return ( + {config.title && } + {/* Files Step */} - {steps.createFilesStep({ + {config.files.isVisible !== false && steps.createFilesStep({ selectedFiles: config.files.selectedFiles, isCollapsed: config.files.isCollapsed, placeholder: config.files.placeholder, diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx new file mode 100644 index 000000000..eb9c9be6d --- /dev/null +++ b/frontend/src/components/tools/shared/renderToolButtons.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Box, Stack } from '@mantine/core'; +import ToolButton from '../toolPicker/ToolButton'; +import SubcategoryHeader from './SubcategoryHeader'; + +import { getSubcategoryLabel } from "../../../data/toolsTaxonomy"; +import { TFunction } from 'i18next'; +import { SubcategoryGroup } from '../../../hooks/useToolSections'; + +// Helper function to render tool buttons for a subcategory +export const renderToolButtons = ( + t: TFunction, + subcategory: SubcategoryGroup, + selectedToolKey: string | null, + onSelect: (id: string) => void, + showSubcategoryHeader: boolean = true +) => ( + + {showSubcategoryHeader && ( + + )} + + {subcategory.tools.map(({ id, tool }) => ( + + ))} + + +); diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx index af668a1fa..66bd9489e 100644 --- a/frontend/src/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -14,9 +14,9 @@ interface ToolButtonProps { const ToolButton: React.FC = ({ id, tool, isSelected, onSelect }) => { const handleClick = (id: string) => { if (tool.link) { - // Open external link in new tab + // Open external link in new tab window.open(tool.link, '_blank', 'noopener,noreferrer'); - return; + return; } // Normal tool selection onSelect(id); @@ -47,4 +47,4 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect ); }; -export default ToolButton; \ No newline at end of file +export default ToolButton; diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index f01a9f87d..774126aa2 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect, useMemo } from "react"; import { Stack, Button, Text } from "@mantine/core"; import { useTranslation } from "react-i18next"; +import LocalIcon from '../../shared/LocalIcon'; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; import { TextInput } from "../../shared/TextInput"; import './ToolPicker.css'; @@ -12,19 +13,26 @@ interface ToolSearchProps { onToolSelect?: (toolId: string) => void; mode: 'filter' | 'dropdown'; selectedToolKey?: string | null; + placeholder?: string; + hideIcon?: boolean; + onFocus?: () => void; } -const ToolSearch = ({ - value, - onChange, - toolRegistry, - onToolSelect, +const ToolSearch = ({ + value, + onChange, + toolRegistry, + onToolSelect, mode = 'filter', - selectedToolKey + selectedToolKey, + placeholder, + hideIcon = false, + onFocus }: ToolSearchProps) => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); const searchRef = useRef(null); + const dropdownRef = useRef(null); const filteredTools = useMemo(() => { if (!value.trim()) return []; @@ -47,7 +55,12 @@ const ToolSearch = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (searchRef.current && !searchRef.current.contains(event.target as Node)) { + if ( + searchRef.current && + dropdownRef.current && + !searchRef.current.contains(event.target as Node) && + !dropdownRef.current.contains(event.target as Node) + ) { setDropdownOpen(false); } }; @@ -61,9 +74,10 @@ const ToolSearch = ({ ref={searchRef} value={value} onChange={handleSearchChange} - placeholder={t("toolPicker.searchPlaceholder", "Search tools...")} - icon={search} + placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} + icon={hideIcon ? undefined : } autoComplete="off" + />
); @@ -77,19 +91,19 @@ const ToolSearch = ({ {searchInput} {dropdownOpen && filteredTools.length > 0 && (
@@ -97,7 +111,10 @@ const ToolSearch = ({