diff --git a/.editorconfig b/.editorconfig
index 5b76408cc..5e5445769 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -24,7 +24,7 @@ indent_size = 2
insert_final_newline = false
trim_trailing_whitespace = false
-[{*.js,*.jsx,*.ts,*.tsx}]
+[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
indent_size = 2
[*.css]
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0e38b82fb..b38abe5dc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -147,6 +147,8 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci
+ - name: Lint frontend
+ run: cd frontend && npm run lint
- name: Build frontend
run: cd frontend && npm run build
- name: Run frontend tests
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 6ab09796f..4f627148e 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -19,5 +19,6 @@
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
+ "dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
]
}
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
new file mode 100644
index 000000000..d823a6af9
--- /dev/null
+++ b/frontend/eslint.config.mjs
@@ -0,0 +1,31 @@
+// @ts-check
+
+import eslint from '@eslint/js';
+import { defineConfig } from 'eslint/config';
+import tseslint from 'typescript-eslint';
+
+export default defineConfig(
+ eslint.configs.recommended,
+ tseslint.configs.recommended,
+ {
+ ignores: [
+ "dist", // Contains 3rd party code
+ "public", // Contains 3rd party code
+ ],
+ },
+ {
+ rules: {
+ "no-undef": "off", // Temporarily disabled until codebase conformant
+ "@typescript-eslint/no-empty-object-type": [
+ "error",
+ {
+ // Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future
+ allowInterfaces: 'with-single-extends',
+ },
+ ],
+ "@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant
+ "@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant
+ "@typescript-eslint/no-unused-vars": "off", // Temporarily disabled until codebase conformant
+ },
+ }
+);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7e6d23d50..342f0512f 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -41,6 +41,7 @@
"web-vitals": "^2.1.4"
},
"devDependencies": {
+ "@eslint/js": "^9.34.0",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify/utils": "^3.0.1",
"@playwright/test": "^1.40.0",
@@ -49,6 +50,7 @@
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^1.0.0",
+ "eslint": "^9.34.0",
"jsdom": "^23.0.0",
"license-checker": "^25.0.1",
"madge": "^8.0.0",
@@ -56,7 +58,8 @@
"postcss-cli": "^11.0.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
- "typescript": "^5.8.3",
+ "typescript": "^5.9.2",
+ "typescript-eslint": "^8.42.0",
"vite": "^6.3.5",
"vitest": "^1.0.0"
}
@@ -1164,6 +1167,173 @@
"node": ">=18"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+ "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
+ "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
+ "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.34.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
+ "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
+ "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.2",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@floating-ui/core": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
@@ -1217,6 +1387,72 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"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",
@@ -2659,6 +2895,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "24.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
@@ -2709,15 +2952,80 @@
"@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==",
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
+ "integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.40.0",
- "@typescript-eslint/types": "^8.40.0",
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.42.0",
+ "@typescript-eslint/type-utils": "8.42.0",
+ "@typescript-eslint/utils": "8.42.0",
+ "@typescript-eslint/visitor-keys": "8.42.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.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-eslint/parser": "^8.42.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz",
+ "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.42.0",
+ "@typescript-eslint/types": "8.42.0",
+ "@typescript-eslint/typescript-estree": "8.42.0",
+ "@typescript-eslint/visitor-keys": "8.42.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": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz",
+ "integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.42.0",
+ "@typescript-eslint/types": "^8.42.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2731,10 +3039,28 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz",
+ "integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.42.0",
+ "@typescript-eslint/visitor-keys": "8.42.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"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==",
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz",
+ "integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2748,10 +3074,35 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz",
+ "integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.42.0",
+ "@typescript-eslint/typescript-estree": "8.42.0",
+ "@typescript-eslint/utils": "8.42.0",
+ "debug": "^4.3.4",
+ "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": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "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==",
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz",
+ "integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2763,16 +3114,16 @@
}
},
"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==",
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz",
+ "integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==",
"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",
+ "@typescript-eslint/project-service": "8.42.0",
+ "@typescript-eslint/tsconfig-utils": "8.42.0",
+ "@typescript-eslint/types": "8.42.0",
+ "@typescript-eslint/visitor-keys": "8.42.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -2817,14 +3168,38 @@
"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==",
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz",
+ "integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.40.0",
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.42.0",
+ "@typescript-eslint/types": "8.42.0",
+ "@typescript-eslint/typescript-estree": "8.42.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz",
+ "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.42.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -3136,6 +3511,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
@@ -3162,6 +3547,23 @@
"node": ">= 6.0.0"
}
},
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -3236,6 +3638,13 @@
"node": ">=10"
}
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -4028,6 +4437,13 @@
"node": ">=4.0.0"
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@@ -4492,6 +4908,84 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eslint": {
+ "version": "9.34.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz",
+ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.3.1",
+ "@eslint/core": "^0.15.2",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.34.0",
+ "@eslint/plugin-kit": "^0.3.5",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@@ -4505,6 +4999,24 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "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",
@@ -4519,6 +5031,32 @@
"node": ">=4"
}
},
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
@@ -4592,6 +5130,13 @@
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"dev": true
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -4622,6 +5167,20 @@
"node": ">= 6"
}
},
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -4638,6 +5197,19 @@
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
@@ -4705,6 +5277,44 @@
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -4971,6 +5581,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -5014,6 +5637,13 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -5246,6 +5876,16 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -5267,6 +5907,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
@@ -5558,6 +6208,19 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/jsdom": {
"version": "23.2.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz",
@@ -5635,12 +6298,33 @@
"node": ">=6"
}
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -5705,12 +6389,36 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"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/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/license-checker": {
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz",
@@ -6105,12 +6813,35 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -6535,6 +7266,13 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -6736,6 +7474,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/ora": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
@@ -6805,6 +7561,51 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "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",
@@ -6869,6 +7670,16 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -7493,6 +8304,16 @@
"node": ">=18"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -9022,6 +9843,19 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/type-detect": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
@@ -9045,9 +9879,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@@ -9058,6 +9892,30 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.42.0.tgz",
+ "integrity": "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.42.0",
+ "@typescript-eslint/parser": "8.42.0",
+ "@typescript-eslint/typescript-estree": "8.42.0",
+ "@typescript-eslint/utils": "8.42.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
@@ -9111,6 +9969,16 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
@@ -10541,6 +11409,16 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 160c96c18..d73e9ad97 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -40,6 +40,7 @@
"predev": "npm run generate-icons",
"dev": "npx tsc --noEmit && vite",
"prebuild": "npm run generate-icons",
+ "lint": "npx eslint",
"build": "npx tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
@@ -72,6 +73,7 @@
]
},
"devDependencies": {
+ "@eslint/js": "^9.34.0",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify/utils": "^3.0.1",
"@playwright/test": "^1.40.0",
@@ -80,6 +82,7 @@
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^1.0.0",
+ "eslint": "^9.34.0",
"jsdom": "^23.0.0",
"license-checker": "^25.0.1",
"madge": "^8.0.0",
@@ -87,7 +90,8 @@
"postcss-cli": "^11.0.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
- "typescript": "^5.8.3",
+ "typescript": "^5.9.2",
+ "typescript-eslint": "^8.42.0",
"vite": "^6.3.5",
"vitest": "^1.0.0"
}
diff --git a/frontend/scripts/generate-licenses.js b/frontend/scripts/generate-licenses.js
index aaac69800..7ab425e19 100644
--- a/frontend/scripts/generate-licenses.js
+++ b/frontend/scripts/generate-licenses.js
@@ -224,7 +224,7 @@ function getLicenseUrl(licenseType) {
// Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
if (licenseType.includes('AND') || licenseType.includes('OR')) {
// Extract the first license from compound expressions for URL
- const match = licenseType.match(/\(?\s*([A-Za-z0-9\-\.]+)/);
+ const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/);
if (match && licenseUrls[match[1]]) {
return licenseUrls[match[1]];
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8a30d3869..c3cbf3e89 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -14,6 +14,9 @@ import "./styles/cookieconsent.css";
import "./index.css";
import { RightRailProvider } from "./contexts/RightRailContext";
+// Import file ID debugging helpers (development only)
+import "./utils/fileIdSafety";
+
// Loading component for i18next suspense
const LoadingFallback = () => (
= ({ selectedTool }) => {
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
// Wrapper for storeFile that generates UUID
- const storeFileWithId = useCallback(async (file: File) => {
+ const storeStirlingFile = useCallback(async (file: File) => {
const fileId = createFileId(); // Generate UUID for storage
return await storeFile(file, fileId);
}, [storeFile]);
diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx
index bf95d9796..901eb20ca 100644
--- a/frontend/src/components/fileEditor/FileEditor.tsx
+++ b/frontend/src/components/fileEditor/FileEditor.tsx
@@ -16,12 +16,12 @@ import styles from './FileEditor.module.css';
import FileEditorThumbnail from './FileEditorThumbnail';
import FilePickerModal from '../shared/FilePickerModal';
import SkeletonLoader from '../shared/SkeletonLoader';
-import { FileId } from '../../types/file';
+import { FileId, StirlingFile } from '../../types/fileContext';
interface FileEditorProps {
- onOpenPageEditor?: (file: File) => void;
- onMergeFiles?: (files: File[]) => void;
+ onOpenPageEditor?: (file: StirlingFile) => void;
+ onMergeFiles?: (files: StirlingFile[]) => void;
toolMode?: boolean;
showUpload?: boolean;
showBulkActions?: boolean;
@@ -50,7 +50,7 @@ const FileEditor = ({
// Extract needed values from state (memoized to prevent infinite loops)
const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]);
- const activeFileRecords = useMemo(() => selectors.getFileRecords(), [selectors.getFilesSignature()]);
+ const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]);
const selectedFileIds = state.ui.selectedFileIds;
const isProcessing = state.ui.isProcessing;
@@ -92,10 +92,10 @@ const FileEditor = ({
const contextSelectedIdsRef = useRef
([]);
contextSelectedIdsRef.current = contextSelectedIds;
- // Use activeFileRecords directly - no conversion needed
+ // Use activeStirlingFileStubs directly - no conversion needed
const localSelectedIds = contextSelectedIds;
- // Helper to convert FileRecord to FileThumbnail format
+ // Helper to convert StirlingFileStub to FileThumbnail format
const recordToFileItem = useCallback((record: any) => {
const file = selectors.getFile(record.id);
if (!file) return null;
@@ -253,26 +253,26 @@ const FileEditor = ({
}, [addFiles]);
const selectAll = useCallback(() => {
- setSelectedFiles(activeFileRecords.map(r => r.id)); // Use FileRecord IDs directly
- }, [activeFileRecords, setSelectedFiles]);
+ setSelectedFiles(activeStirlingFileStubs.map(r => r.id)); // Use StirlingFileStub IDs directly
+ }, [activeStirlingFileStubs, setSelectedFiles]);
const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]);
const closeAllFiles = useCallback(() => {
- if (activeFileRecords.length === 0) return;
+ if (activeStirlingFileStubs.length === 0) return;
// Remove all files from context but keep in storage
- const allFileIds = activeFileRecords.map(record => record.id);
+ const allFileIds = activeStirlingFileStubs.map(record => record.id);
removeFiles(allFileIds, false); // false = keep in storage
// Clear selections
setSelectedFiles([]);
- }, [activeFileRecords, removeFiles, setSelectedFiles]);
+ }, [activeStirlingFileStubs, removeFiles, setSelectedFiles]);
const toggleFile = useCallback((fileId: FileId) => {
const currentSelectedIds = contextSelectedIdsRef.current;
- const targetRecord = activeFileRecords.find(r => r.id === fileId);
+ const targetRecord = activeStirlingFileStubs.find(r => r.id === fileId);
if (!targetRecord) return;
const contextFileId = fileId; // No need to create a new ID
@@ -302,7 +302,7 @@ const FileEditor = ({
// Update context (this automatically updates tool selection since they use the same action)
setSelectedFiles(newSelection);
- }, [setSelectedFiles, toolMode, setStatus, activeFileRecords]);
+ }, [setSelectedFiles, toolMode, setStatus, activeStirlingFileStubs]);
const toggleSelectionMode = useCallback(() => {
setSelectionMode(prev => {
@@ -316,7 +316,7 @@ const FileEditor = ({
// File reordering handler for drag and drop
const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => {
- const currentIds = activeFileRecords.map(r => r.id);
+ const currentIds = activeStirlingFileStubs.map(r => r.id);
// Find indices
const sourceIndex = currentIds.findIndex(id => id === sourceFileId);
@@ -368,13 +368,13 @@ const FileEditor = ({
// Update status
const moveCount = filesToMove.length;
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
- }, [activeFileRecords, reorderFiles, setStatus]);
+ }, [activeStirlingFileStubs, reorderFiles, setStatus]);
// File operations using context
const handleDeleteFile = useCallback((fileId: FileId) => {
- const record = activeFileRecords.find(r => r.id === fileId);
+ const record = activeStirlingFileStubs.find(r => r.id === fileId);
const file = record ? selectors.getFile(record.id) : null;
if (record && file) {
@@ -405,27 +405,27 @@ const FileEditor = ({
const currentSelected = selectedFileIds.filter(id => id !== contextFileId);
setSelectedFiles(currentSelected);
}
- }, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
+ }, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
const handleViewFile = useCallback((fileId: FileId) => {
- const record = activeFileRecords.find(r => r.id === fileId);
+ const record = activeStirlingFileStubs.find(r => r.id === fileId);
if (record) {
// Set the file as selected in context and switch to viewer for preview
setSelectedFiles([fileId]);
navActions.setWorkbench('viewer');
}
- }, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
+ }, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]);
const handleMergeFromHere = useCallback((fileId: FileId) => {
- const startIndex = activeFileRecords.findIndex(r => r.id === fileId);
+ const startIndex = activeStirlingFileStubs.findIndex(r => r.id === fileId);
if (startIndex === -1) return;
- const recordsToMerge = activeFileRecords.slice(startIndex);
- const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as File[];
+ const recordsToMerge = activeStirlingFileStubs.slice(startIndex);
+ const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as StirlingFile[];
if (onMergeFiles) {
onMergeFiles(filesToMerge);
}
- }, [activeFileRecords, selectors, onMergeFiles]);
+ }, [activeStirlingFileStubs, selectors, onMergeFiles]);
const handleSplitFile = useCallback((fileId: FileId) => {
const file = selectors.getFile(fileId);
@@ -467,7 +467,7 @@ const FileEditor = ({
- {activeFileRecords.length === 0 && !zipExtractionProgress.isExtracting ? (
+ {activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
📁
@@ -475,7 +475,7 @@ const FileEditor = ({
Upload PDF files, ZIP archives, or load from storage to get started
- ) : activeFileRecords.length === 0 && zipExtractionProgress.isExtracting ? (
+ ) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? (
@@ -522,7 +522,7 @@ const FileEditor = ({
pointerEvents: 'auto'
}}
>
- {activeFileRecords.map((record, index) => {
+ {activeStirlingFileStubs.map((record, index) => {
const fileItem = recordToFileItem(record);
if (!fileItem) return null;
@@ -531,7 +531,7 @@ const FileEditor = ({
key={record.id}
file={fileItem}
index={index}
- totalFiles={activeFileRecords.length}
+ totalFiles={activeStirlingFileStubs.length}
selectedFiles={localSelectedIds}
selectionMode={selectionMode}
onToggleFile={toggleFile}
diff --git a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
index e82483898..bfeb404c5 100644
--- a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
+++ b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
@@ -61,8 +61,8 @@ const FileEditorThumbnail = ({
// Resolve the actual File object for pin/unpin operations
const actualFile = useMemo(() => {
- return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
- }, [activeFiles, file.name, file.size]);
+ return activeFiles.find(f => f.fileId === file.id);
+ }, [activeFiles, file.id]);
const isPinned = actualFile ? isFilePinned(actualFile) : false;
const downloadSelectedFile = useCallback(() => {
diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx
index 1eda1f6c8..ad81ce463 100644
--- a/frontend/src/components/pageEditor/FileThumbnail.tsx
+++ b/frontend/src/components/pageEditor/FileThumbnail.tsx
@@ -61,8 +61,8 @@ const FileThumbnail = ({
// Resolve the actual File object for pin/unpin operations
const actualFile = useMemo(() => {
- return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
- }, [activeFiles, file.name, file.size]);
+ return activeFiles.find(f => f.fileId === file.id);
+ }, [activeFiles, file.id]);
const isPinned = actualFile ? isFilePinned(actualFile) : false;
const downloadSelectedFile = useCallback(() => {
diff --git a/frontend/src/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/components/pageEditor/hooks/usePageDocument.ts
index b620c87b8..3a4d49053 100644
--- a/frontend/src/components/pageEditor/hooks/usePageDocument.ts
+++ b/frontend/src/components/pageEditor/hooks/usePageDocument.ts
@@ -27,9 +27,9 @@ export function usePageDocument(): PageDocumentHook {
const globalProcessing = state.ui.isProcessing;
// Get primary file record outside useMemo to track processedFile changes
- const primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null;
- const processedFilePages = primaryFileRecord?.processedFile?.pages;
- const processedFileTotalPages = primaryFileRecord?.processedFile?.totalPages;
+ const primaryStirlingFileStub = primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : null;
+ const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
+ const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
// Compute merged document with stable signature (prevents infinite loops)
const mergedPdfDocument = useMemo((): PDFDocument | null => {
@@ -38,16 +38,16 @@ export function usePageDocument(): PageDocumentHook {
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
// If we have file IDs but no file record, something is wrong - return null to show loading
- if (!primaryFileRecord) {
+ if (!primaryStirlingFileStub) {
console.log('🎬 PageEditor: No primary file record found, showing loading');
return null;
}
const name =
activeFileIds.length === 1
- ? (primaryFileRecord.name ?? 'document.pdf')
+ ? (primaryStirlingFileStub.name ?? 'document.pdf')
: activeFileIds
- .map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
+ .map(id => (selectors.getStirlingFileStub(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
.join(' + ');
// Build page insertion map from files with insertion positions
@@ -55,7 +55,7 @@ export function usePageDocument(): PageDocumentHook {
const originalFileIds: FileId[] = [];
activeFileIds.forEach(fileId => {
- const record = selectors.getFileRecord(fileId);
+ const record = selectors.getStirlingFileStub(fileId);
if (record?.insertAfterPageId !== undefined) {
if (!insertionMap.has(record.insertAfterPageId)) {
insertionMap.set(record.insertAfterPageId, []);
@@ -72,12 +72,12 @@ export function usePageDocument(): PageDocumentHook {
// Helper function to create pages from a file
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
- const fileRecord = selectors.getFileRecord(fileId);
- if (!fileRecord) {
+ const stirlingFileStub = selectors.getStirlingFileStub(fileId);
+ if (!stirlingFileStub) {
return [];
}
- const processedFile = fileRecord.processedFile;
+ const processedFile = stirlingFileStub.processedFile;
let filePages: PDFPage[] = [];
if (processedFile?.pages && processedFile.pages.length > 0) {
@@ -159,7 +159,7 @@ export function usePageDocument(): PageDocumentHook {
};
return mergedDoc;
- }, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
+ }, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
// Large document detection for smart loading
const isVeryLargeDocument = useMemo(() => {
diff --git a/frontend/src/components/shared/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx
index 73ae01dba..6c63af42e 100644
--- a/frontend/src/components/shared/FileCard.tsx
+++ b/frontend/src/components/shared/FileCard.tsx
@@ -6,13 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage";
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditIcon from "@mui/icons-material/Edit";
-import { FileRecord } from "../../types/fileContext";
+import { StirlingFileStub } from "../../types/fileContext";
import { getFileSize, getFileDate } from "../../utils/fileUtils";
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
interface FileCardProps {
file: File;
- record?: FileRecord;
+ record?: StirlingFileStub;
onRemove: () => void;
onDoubleClick?: () => void;
onView?: () => void;
@@ -25,7 +25,7 @@ interface FileCardProps {
const FileCard = ({ file, record, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
const { t } = useTranslation();
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
- const fileMetadata = record ? { id: record.id, name: file.name, type: file.type, size: file.size, lastModified: file.lastModified } : null;
+ const fileMetadata = record ? { id: record.id, name: record.name, type: record.type, size: record.size, lastModified: record.lastModified } : null;
const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileMetadata);
const thumb = record?.thumbnailUrl || indexedDBThumb;
const [isHovered, setIsHovered] = useState(false);
diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx
index 1a43196d6..2d9cba640 100644
--- a/frontend/src/components/shared/FileGrid.tsx
+++ b/frontend/src/components/shared/FileGrid.tsx
@@ -4,15 +4,15 @@ import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
import FileCard from "./FileCard";
-import { FileRecord } from "../../types/fileContext";
+import { StirlingFileStub } from "../../types/fileContext";
import { FileId } from "../../types/file";
interface FileGridProps {
- files: Array<{ file: File; record?: FileRecord }>;
+ files: Array<{ file: File; record?: StirlingFileStub }>;
onRemove?: (index: number) => void;
- onDoubleClick?: (item: { file: File; record?: FileRecord }) => void;
- onView?: (item: { file: File; record?: FileRecord }) => void;
- onEdit?: (item: { file: File; record?: FileRecord }) => void;
+ onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void;
+ onView?: (item: { file: File; record?: StirlingFileStub }) => void;
+ onEdit?: (item: { file: File; record?: StirlingFileStub }) => void;
onSelect?: (fileId: FileId) => void;
selectedFiles?: FileId[];
showSearch?: boolean;
@@ -123,9 +123,17 @@ const FileGrid = ({
h="30rem"
style={{ overflowY: "auto", width: "100%" }}
>
- {displayFiles.map((item, idx) => {
- const fileId = item.record?.id || item.file.name as FileId /* FIX ME: This doesn't seem right */;
- const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId);
+ {displayFiles
+ .filter(item => {
+ if (!item.record?.id) {
+ console.error('FileGrid: File missing StirlingFileStub with proper ID:', item.file.name);
+ return false;
+ }
+ return true;
+ })
+ .map((item, idx) => {
+ const fileId = item.record!.id; // Safe to assert after filter
+ const originalIdx = files.findIndex(f => f.record?.id === fileId);
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
return (
(({
-}, ref) => {
+const QuickAccessBar = forwardRef((_, ref) => {
const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext();
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx
index b141ec493..24f927443 100644
--- a/frontend/src/components/shared/RightRail.tsx
+++ b/frontend/src/components/shared/RightRail.tsx
@@ -34,7 +34,6 @@ export default function RightRail() {
const activeFiles = selectors.getFiles();
const filesSignature = selectors.getFilesSignature();
- const fileRecords = selectors.getFileRecords();
// Compute selection state and total items
const getSelectionState = useCallback(() => {
@@ -85,7 +84,7 @@ export default function RightRail() {
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);
@@ -206,8 +205,8 @@ export default function RightRail() {
)}
{/* Group: Selection controls + Close, animate as one unit when entering/leaving viewer */}
-
@@ -358,14 +357,14 @@ export default function RightRail() {
0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All'))
} position="left" offset={12} arrow>
-
{
cancelAnimationFrame(raf);
- try { ro.disconnect(); } catch {}
- try { mo.disconnect(); } catch {}
+ try { ro.disconnect(); } catch { /* Ignore errors */ }
+ try { mo.disconnect(); } catch { /* Ignore errors */ }
};
}
diff --git a/frontend/src/components/tools/convert/ConvertSettings.tsx b/frontend/src/components/tools/convert/ConvertSettings.tsx
index bae2954a9..3a019f8da 100644
--- a/frontend/src/components/tools/convert/ConvertSettings.tsx
+++ b/frontend/src/components/tools/convert/ConvertSettings.tsx
@@ -22,13 +22,13 @@ import {
OUTPUT_OPTIONS,
FIT_OPTIONS
} from "../../../constants/convertConstants";
-import { FileId } from "../../../types/file";
+import { StirlingFile } from "../../../types/fileContext";
interface ConvertSettingsProps {
parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
disabled?: boolean;
}
@@ -129,7 +129,7 @@ const ConvertSettings = ({
};
const filterFilesByExtension = (extension: string) => {
- const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as File[];
+ const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as StirlingFile[];
return files.filter(file => {
const fileExtension = detectFileExtension(file.name);
@@ -143,21 +143,8 @@ const ConvertSettings = ({
});
};
- const updateFileSelection = (files: File[]) => {
- // Map File objects to their actual IDs in FileContext
- const fileIds = files.map(file => {
- // Find the file ID by matching file properties
- const fileRecord = state.files.ids
- .map(id => selectors.getFileRecord(id))
- .find(record =>
- record &&
- record.name === file.name &&
- record.size === file.size &&
- record.lastModified === file.lastModified
- );
- return fileRecord?.id;
- }).filter((id): id is FileId => id !== undefined); // Type guard to ensure only strings
-
+ const updateFileSelection = (files: StirlingFile[]) => {
+ const fileIds = files.map(file => file.fileId);
setSelectedFiles(fileIds);
};
diff --git a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx
index e1a662bd2..49e057a1c 100644
--- a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx
+++ b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx
@@ -3,11 +3,12 @@ import { Stack, Text, Select, Alert } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection';
+import { StirlingFile } from '../../../types/fileContext';
interface ConvertToPdfaSettingsProps {
parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
disabled?: boolean;
}
diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx
index 9b375fc2f..a083d65ef 100644
--- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx
+++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx
@@ -6,9 +6,10 @@ import UploadIcon from '@mui/icons-material/Upload';
import { useFilesModalContext } from "../../../contexts/FilesModalContext";
import { useAllFiles } from "../../../contexts/FileContext";
import { useFileManager } from "../../../hooks/useFileManager";
+import { StirlingFile } from "../../../types/fileContext";
export interface FileStatusIndicatorProps {
- selectedFiles?: File[];
+ selectedFiles?: StirlingFile[];
placeholder?: string;
}
@@ -17,7 +18,7 @@ const FileStatusIndicator = ({
}: FileStatusIndicatorProps) => {
const { t } = useTranslation();
const { openFilesModal, onFilesSelect } = useFilesModalContext();
- const { files: workbenchFiles } = useAllFiles();
+ const { files: stirlingFileStubs } = useAllFiles();
const { loadRecentFiles } = useFileManager();
const [hasRecentFiles, setHasRecentFiles] = useState(null);
@@ -55,7 +56,7 @@ const FileStatusIndicator = ({
}
// Check if there are no files in the workbench
- if (workbenchFiles.length === 0) {
+ if (stirlingFileStubs.length === 0) {
// If no recent files, show upload button
if (!hasRecentFiles) {
return (
diff --git a/frontend/src/components/tools/shared/FilesToolStep.tsx b/frontend/src/components/tools/shared/FilesToolStep.tsx
index b062e9c02..8c188d4a9 100644
--- a/frontend/src/components/tools/shared/FilesToolStep.tsx
+++ b/frontend/src/components/tools/shared/FilesToolStep.tsx
@@ -1,9 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import FileStatusIndicator from './FileStatusIndicator';
+import { StirlingFile } from '../../../types/fileContext';
export interface FilesToolStepProps {
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
isCollapsed?: boolean;
onCollapsedClick?: () => void;
placeholder?: string;
diff --git a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx
index fca3b5e56..c1299a749 100644
--- a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx
+++ b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx
@@ -3,8 +3,6 @@ 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 {
const { t } = useTranslation();
const suggestedTools = useSuggestedTools();
diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx
index b6a7594c6..83057c13a 100644
--- a/frontend/src/components/tools/shared/createToolFlow.tsx
+++ b/frontend/src/components/tools/shared/createToolFlow.tsx
@@ -4,9 +4,10 @@ import { createToolSteps, ToolStepProvider } from './ToolStep';
import OperationButton from './OperationButton';
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
+import { StirlingFile } from '../../../types/fileContext';
export interface FilesStepConfig {
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
isCollapsed?: boolean;
placeholder?: string;
onCollapsedClick?: () => void;
diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx
index 53a01cb77..d4350044e 100644
--- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx
+++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx
@@ -126,7 +126,7 @@ const ToolSearch = ({
key={id}
variant="subtle"
onClick={() => {
- onToolSelect && onToolSelect(id);
+ onToolSelect?.(id);
setDropdownOpen(false);
}}
leftSection={{tool.icon}
}
diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx
index 0932e995b..53d9e98e2 100644
--- a/frontend/src/components/viewer/Viewer.tsx
+++ b/frontend/src/components/viewer/Viewer.tsx
@@ -15,6 +15,7 @@ import { fileStorage } from "../../services/fileStorage";
import SkeletonLoader from '../shared/SkeletonLoader';
import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext";
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
+import { isFileObject } from "../../types/fileContext";
import { FileId } from "../../types/file";
@@ -201,7 +202,7 @@ const Viewer = ({
const effectiveFile = React.useMemo(() => {
if (previewFile) {
// Validate the preview file
- if (!(previewFile instanceof File)) {
+ if (!isFileObject(previewFile)) {
return null;
}
diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx
index 39faa0643..80735de58 100644
--- a/frontend/src/contexts/FileContext.tsx
+++ b/frontend/src/contexts/FileContext.tsx
@@ -19,7 +19,10 @@ import {
FileContextStateValue,
FileContextActionsValue,
FileContextActions,
- FileRecord
+ FileId,
+ StirlingFileStub,
+ StirlingFile,
+ createStirlingFile
} from '../types/fileContext';
// Import modular components
@@ -29,7 +32,6 @@ import { AddedFile, addFiles, consumeFiles, undoConsumeFiles, createFileActions
import { FileLifecycleManager } from './file/lifecycle';
import { FileStateContext, FileActionsContext } from './file/contexts';
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
-import { FileId } from '../types/file';
const DEBUG = process.env.NODE_ENV === 'development';
@@ -79,7 +81,7 @@ function FileContextInner({
}
// File operations using unified addFiles helper with persistence
- const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => {
+ const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => {
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
// Auto-select the newly added files if requested
@@ -98,15 +100,15 @@ function FileContextInner({
}));
}
- return addedFilesWithIds.map(({ file }) => file);
+ return addedFilesWithIds.map(({ file, id }) => createStirlingFile(file, id));
}, [indexedDB, enablePersistence]);
- const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => {
+ const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => {
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
- return result.map(({ file }) => file);
+ return result.map(({ file, id }) => createStirlingFile(file, id));
}, []);
- const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => {
+ const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => {
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
// Auto-select the newly added files if requested
@@ -114,7 +116,7 @@ function FileContextInner({
selectFiles(result);
}
- return result.map(({ file }) => file);
+ return result.map(({ file, id }) => createStirlingFile(file, id));
}, []);
// Action creators
@@ -122,11 +124,11 @@ function FileContextInner({
// Helper functions for pinned files
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise => {
- return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB);
+ return consumeFiles(inputFileIds, outputFiles, filesRef, dispatch, indexedDB);
}, [indexedDB]);
- const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]): Promise => {
- return undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds, stateRef, filesRef, dispatch, indexedDB);
+ const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise => {
+ return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, stateRef, filesRef, dispatch, indexedDB);
}, [indexedDB]);
// Helper to find FileId from File object
@@ -140,24 +142,14 @@ function FileContextInner({
});
}, []);
- // File-to-ID wrapper functions for pinning
- const pinFileWrapper = useCallback((file: File) => {
- const fileId = findFileId(file);
- if (fileId) {
- baseActions.pinFile(fileId);
- } else {
- console.warn('File not found for pinning:', file.name);
- }
- }, [baseActions, findFileId]);
+ // File pinning functions - use StirlingFile directly
+ const pinFileWrapper = useCallback((file: StirlingFile) => {
+ baseActions.pinFile(file.fileId);
+ }, [baseActions]);
- const unpinFileWrapper = useCallback((file: File) => {
- const fileId = findFileId(file);
- if (fileId) {
- baseActions.unpinFile(fileId);
- } else {
- console.warn('File not found for unpinning:', file.name);
- }
- }, [baseActions, findFileId]);
+ const unpinFileWrapper = useCallback((file: StirlingFile) => {
+ baseActions.unpinFile(file.fileId);
+ }, [baseActions]);
// Complete actions object
const actions = useMemo(() => ({
@@ -178,8 +170,8 @@ function FileContextInner({
}
}
},
- updateFileRecord: (fileId: FileId, updates: Partial) =>
- lifecycleManager.updateFileRecord(fileId, updates, stateRef),
+ updateStirlingFileStub: (fileId: FileId, updates: Partial) =>
+ lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef),
reorderFiles: (orderedFileIds: FileId[]) => {
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
},
@@ -303,7 +295,7 @@ export {
useFileSelection,
useFileManagement,
useFileUI,
- useFileRecord,
+ useStirlingFileStub,
useAllFiles,
useSelectedFiles,
// Primary API hooks for tools
diff --git a/frontend/src/contexts/file/FileReducer.ts b/frontend/src/contexts/file/FileReducer.ts
index 93c06c4d2..4c4196764 100644
--- a/frontend/src/contexts/file/FileReducer.ts
+++ b/frontend/src/contexts/file/FileReducer.ts
@@ -6,7 +6,7 @@ import { FileId } from '../../types/file';
import {
FileContextState,
FileContextAction,
- FileRecord
+ StirlingFileStub
} from '../../types/fileContext';
// Initial state
@@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = {
function processFileSwap(
state: FileContextState,
filesToRemove: FileId[],
- filesToAdd: FileRecord[]
+ filesToAdd: StirlingFileStub[]
): FileContextState {
// Only remove unpinned files
const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id));
@@ -70,11 +70,11 @@ function processFileSwap(
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
switch (action.type) {
case 'ADD_FILES': {
- const { fileRecords } = action.payload;
+ const { stirlingFileStubs } = action.payload;
const newIds: FileId[] = [];
- const newById: Record = { ...state.files.byId };
+ const newById: Record = { ...state.files.byId };
- fileRecords.forEach(record => {
+ stirlingFileStubs.forEach(record => {
// Only add if not already present (dedupe by stable ID)
if (!newById[record.id]) {
newIds.push(record.id);
@@ -233,13 +233,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
}
case 'CONSUME_FILES': {
- const { inputFileIds, outputFileRecords } = action.payload;
- return processFileSwap(state, inputFileIds, outputFileRecords);
+ const { inputFileIds, outputStirlingFileStubs } = action.payload;
+ return processFileSwap(state, inputFileIds, outputStirlingFileStubs);
}
case 'UNDO_CONSUME_FILES': {
- const { inputFileRecords, outputFileIds } = action.payload;
- return processFileSwap(state, outputFileIds, inputFileRecords);
+ const { inputStirlingFileStubs, outputFileIds } = action.payload;
+ return processFileSwap(state, outputFileIds, inputStirlingFileStubs);
}
case 'RESET_CONTEXT': {
diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts
index e55108553..3901fabee 100644
--- a/frontend/src/contexts/file/fileActions.ts
+++ b/frontend/src/contexts/file/fileActions.ts
@@ -3,10 +3,10 @@
*/
import {
- FileRecord,
+ StirlingFileStub,
FileContextAction,
FileContextState,
- toFileRecord,
+ toStirlingFileStub,
createFileId,
createQuickKey
} from '../../types/fileContext';
@@ -109,8 +109,8 @@ export async function addFiles(
await addFilesMutex.lock();
try {
- const fileRecords: FileRecord[] = [];
- const addedFiles: AddedFile[] = [];
+ const stirlingFileStubs: StirlingFileStub[] = [];
+ const addedFiles: AddedFile[] = [];
// Build quickKey lookup from existing files for deduplication
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
@@ -163,7 +163,7 @@ export async function addFiles(
}
// Create record with immediate thumbnail and page metadata
- const record = toFileRecord(file, fileId);
+ const record = toStirlingFileStub(file, fileId);
if (thumbnail) {
record.thumbnailUrl = thumbnail;
// Track blob URLs for cleanup (images return blob URLs that need revocation)
@@ -184,7 +184,7 @@ export async function addFiles(
}
existingQuickKeys.add(quickKey);
- fileRecords.push(record);
+ stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail });
}
break;
@@ -205,7 +205,7 @@ export async function addFiles(
const fileId = createFileId();
filesRef.current.set(fileId, file);
- const record = toFileRecord(file, fileId);
+ const record = toStirlingFileStub(file, fileId);
if (thumbnail) {
record.thumbnailUrl = thumbnail;
// Track blob URLs for cleanup (images return blob URLs that need revocation)
@@ -226,7 +226,7 @@ export async function addFiles(
}
existingQuickKeys.add(quickKey);
- fileRecords.push(record);
+ stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail });
}
break;
@@ -254,7 +254,7 @@ export async function addFiles(
filesRef.current.set(fileId, file);
- const record = toFileRecord(file, fileId);
+ const record = toStirlingFileStub(file, fileId);
// Generate processedFile metadata for stored files
let pageCount: number = 1;
@@ -301,7 +301,7 @@ export async function addFiles(
}
existingQuickKeys.add(quickKey);
- fileRecords.push(record);
+ stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail });
}
@@ -310,9 +310,9 @@ export async function addFiles(
}
// Dispatch ADD_FILES action if we have new files
- if (fileRecords.length > 0) {
- dispatch({ type: 'ADD_FILES', payload: { fileRecords } });
- if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${fileRecords.length} files`);
+ if (stirlingFileStubs.length > 0) {
+ dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs } });
+ if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${stirlingFileStubs.length} files`);
}
return addedFiles;
@@ -328,7 +328,7 @@ export async function addFiles(
async function processFilesIntoRecords(
files: File[],
filesRef: React.MutableRefObject