diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7d6fe..a2a58c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ All notable changes to `pdf-tools` are documented here. The project follows a pragmatic versioning scheme while the app is still below `1.0.0`: minor versions mark coherent user-facing milestones; patch versions mark fixes and small improvements. +## Unreleased + +### Added + +- Added TypeScript type-check, ESLint, Prettier, and aggregate `check` scripts. +- Added ESLint flat config with TypeScript, React Hooks, React Refresh, browser, and Node config support. +- Added Prettier configuration and ignore file. +- Added Vite client type declarations for worker URL imports. +- Added Vitest-based test scripts for one-off and watch-mode test runs. +- Added pure tests for workspace command cloning, snapshot command stability, and serializable command record round-tripping. +- Added hook-level tests for workspace load/replace behavior, command execution, undo, redo, history, redo clearing, dirty-state updates, and content-change callbacks. + +### Changed + +- Marked the package as an ES module package to remove the Vite CJS Node API deprecation warning during local tooling runs. +- Ran Prettier across the project after adding the formatting configuration. +- Split the former monolithic `ReorderPanel` into focused page-workspace components: `PageGrid`, `PageCard`, `PageSelectionToolbar`, `DropIndicator`, and `CopyPagesDialog`. +- Kept drag/drop move, Ctrl/⌘ copy-drag, selection, rotation, deletion, preview opening, and copy-by-position behavior wired through the existing `ReorderPanel` API. +- Extracted generated PDF download URL creation and cleanup for split, extract, and export results into `src/hooks/usePdfGeneratedOutputs.ts`. +- Updated `ActionsPanel` to render prepared download objects instead of creating object URLs during render. +- Extracted workspace page, selection, dirty-state, message, undo/redo history, command creation, command execution, and reset/load helpers from `App.tsx` into `src/workspace/useWorkspaceState.ts`. +- Extracted thumbnail state, caching, invalidation, progressive rendering, rotation-aware rendering, copied-page thumbnail reuse, and thumbnail error reporting into `src/pdf/usePdfThumbnails.ts`. +- Kept PDF loading, IndexedDB persistence, dialogs, preview, merge, export, and split orchestration in `App.tsx` for now. + +### Fixed + +- Fixed existing `tsc --noEmit` failures for Vite worker URL imports and `Uint8Array`/`BlobPart` PDF byte handling. +- Removed a duplicate copy-dialog validation error assignment in `ReorderPanel`. +- Rotated thumbnails from loaded/saved workspaces are now regenerated from the actual current page rotation instead of relying only on rotation changes after load. +- Copied/duplicated pages now receive thumbnails through the shared thumbnail hook/cache path instead of ad-hoc copy handling in `App.tsx`. + ## 0.2.0 — Browser-only PDF workspace baseline ### Added @@ -18,4 +49,4 @@ The project follows a pragmatic versioning scheme while the app is still below ` ### Notes -This release is the baseline for the next refactoring phase. The goal of the upcoming internal changes is to preserve behavior while extracting workspace state, thumbnail handling, generated object URLs, UI subcomponents, tests, and linting into clearer modules. \ No newline at end of file +This release is the baseline for the next refactoring phase. The goal of the upcoming internal changes is to preserve behavior while extracting workspace state, thumbnail handling, generated object URLs, UI subcomponents, tests, and linting into clearer modules. diff --git a/README b/README index 850eb5f..0631671 100644 --- a/README +++ b/README @@ -75,17 +75,17 @@ This makes the project especially useful for self-hosted environments, public-se ### Keyboard shortcuts -| Shortcut | Action | -| --- | --- | -| `F1` / `?` | Open in-app help and tutorial | -| `Ctrl`/`⌘` + `A` | Select all pages | -| `Delete` / `Backspace` | Delete selected pages after confirmation | -| `Esc` | Clear the current selection or close an open dialog | -| `Ctrl`/`⌘` + `Z` | Undo | -| `Ctrl`/`⌘` + `Shift` + `Z` | Redo | -| `Ctrl`/`⌘` + `Y` | Redo | -| `←` / `→` in preview | Move to previous / next page | -| `Esc` in preview | Close preview | +| Shortcut | Action | +| -------------------------- | --------------------------------------------------- | +| `F1` / `?` | Open in-app help and tutorial | +| `Ctrl`/`⌘` + `A` | Select all pages | +| `Delete` / `Backspace` | Delete selected pages after confirmation | +| `Esc` | Clear the current selection or close an open dialog | +| `Ctrl`/`⌘` + `Z` | Undo | +| `Ctrl`/`⌘` + `Shift` + `Z` | Redo | +| `Ctrl`/`⌘` + `Y` | Redo | +| `←` / `→` in preview | Move to previous / next page | +| `Esc` in preview | Close preview | Keyboard shortcuts are ignored while typing in form fields. @@ -115,25 +115,35 @@ browser <-- HTTPS --> static web server / reverse proxy --> built app assets There is no application server to operate. Administrators only need to host the built files from `dist/`. -### Build from source +### Build and test from source ```bash npm ci +npm run check +``` + +`npm run check` runs the main project quality gate: + +```bash +npm run typecheck +npm run lint +npm run test npm run build ``` The production build is written to `dist/`. -To preview the production build locally: +Useful individual development commands: ```bash -npm run preview -``` - -For development: - -```bash -npm run dev +npm run dev # start the Vite development server +npm run preview # preview the production build locally +npm run test # run tests once +npm run test:watch # run tests in watch mode +npm run typecheck # run TypeScript without emitting files +npm run lint # run ESLint +npm run format # format the project with Prettier +npm run format:check # verify Prettier formatting ``` ### Static hosting @@ -219,11 +229,13 @@ The current development baseline is: v0.2.0 — Browser-only PDF workspace baseline ``` +This baseline is preserved through a staged refactoring path. Workspace state, thumbnail handling, generated download URLs, page-grid components, tests, type-checking, linting, and formatting are now separated enough to support the next feature phase without turning `App.tsx` back into a monolith. + ## Project structure ```text src/ - App.tsx Main application state and orchestration + App.tsx Main application orchestration and UI wiring components/ ActionDialog.tsx Reusable confirmation/action dialog ActionsPanel.tsx Export, extract, and split actions diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..5388e5a --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,55 @@ +import js from "@eslint/js"; +import eslintConfigPrettier from "eslint-config-prettier"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["dist", "coverage", "node_modules"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.browser, + ...globals.es2022, + }, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "react-hooks/set-state-in-effect": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, + { + files: ["*.config.{js,ts}", "eslint.config.js"], + languageOptions: { + globals: { + ...globals.node, + ...globals.es2022, + }, + }, + }, + eslintConfigPrettier, +); diff --git a/package-lock.json b/package-lock.json index 422f29f..185edff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pdf-tools", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pdf-tools", - "version": "0.2.0", + "version": "0.2.1", "dependencies": { "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.6.82", @@ -14,402 +14,811 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.8.0", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.2", - "@vitejs/plugin-react-swc": "^3.7.0", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "prettier": "^3.8.3", "typescript": "^5.6.3", - "vite": "^5.4.10" + "typescript-eslint": "^8.59.3", + "vite": "^8.0.13", + "vitest": "^4.1.6" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/@emnapi/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=12" + "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/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "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": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { - "node": ">=12" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, "engines": { - "node": ">=12" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "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": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@napi-rs/canvas": { @@ -597,6 +1006,35 @@ "node": ">= 10" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pdf-lib/standard-fonts": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", @@ -615,31 +1053,10 @@ "pako": "^1.0.10" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", "cpu": [ "arm64" ], @@ -648,12 +1065,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", "cpu": [ "arm64" ], @@ -662,12 +1082,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", "cpu": [ "x64" ], @@ -676,26 +1099,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", "cpu": [ "x64" ], @@ -704,12 +1116,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", "cpu": [ "arm" ], @@ -718,26 +1133,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", "cpu": [ "arm64" ], @@ -746,12 +1150,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", "cpu": [ "arm64" ], @@ -760,40 +1167,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", "cpu": [ "ppc64" ], @@ -802,54 +1184,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", "cpu": [ "s390x" ], @@ -858,12 +1201,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", "cpu": [ "x64" ], @@ -872,12 +1218,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", "cpu": [ "x64" ], @@ -886,26 +1235,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", "cpu": [ "arm64" ], @@ -914,12 +1252,34 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", "cpu": [ "arm64" ], @@ -928,26 +1288,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", "cpu": [ "x64" ], @@ -956,248 +1305,132 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@swc/core": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", - "integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", + "peer": true, "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" }, "engines": { - "node": ">=10" + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.3", - "@swc/core-darwin-x64": "1.15.3", - "@swc/core-linux-arm-gnueabihf": "1.15.3", - "@swc/core-linux-arm64-gnu": "1.15.3", - "@swc/core-linux-arm64-musl": "1.15.3", - "@swc/core-linux-x64-gnu": "1.15.3", - "@swc/core-linux-x64-musl": "1.15.3", - "@swc/core-win32-arm64-msvc": "1.15.3", - "@swc/core-win32-ia32-msvc": "1.15.3", - "@swc/core-win32-x64-msvc": "1.15.3" + "engines": { + "node": ">=18" }, "peerDependencies": { - "@swc/helpers": ">=0.5.17" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { - "@swc/helpers": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { "optional": true } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz", - "integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==", - "cpu": [ - "arm64" - ], + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz", - "integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==", - "cpu": [ - "x64" - ], + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz", - "integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz", - "integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz", - "integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz", - "integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz", - "integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz", - "integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz", - "integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz", - "integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", - "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3" + "tslib": "^2.4.0" } }, + "node_modules/@tybys/wasm-util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1205,6 +1438,24 @@ "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": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1230,22 +1481,604 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", - "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.27", - "@swc/core": "^1.12.11" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.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.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "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.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "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.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "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.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "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.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "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": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "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/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, "node_modules/csstype": { @@ -1255,45 +2088,438 @@ "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.357", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", + "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.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", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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", + "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", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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-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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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/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.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1309,12 +2535,525 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/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/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "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", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "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-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", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1327,6 +3066,66 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1346,12 +3145,127 @@ "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-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "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/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": { + "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/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT" + }, "node_modules/pdf-lib": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", @@ -1383,6 +3297,20 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", @@ -1412,6 +3340,57 @@ "node": "^10 || ^12 || >=14" } }, + "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/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1430,6 +3409,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1438,49 +3418,75 @@ "react": "^18.3.1" } }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, "node_modules/scheduler": { @@ -1492,6 +3498,46 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1502,18 +3548,156 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1522,23 +3706,107 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "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/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -1547,23 +3815,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { "optional": true }, - "lightningcss": { + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -1580,8 +3858,283 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } + }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "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/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index d91b5ff..c08dd00 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,19 @@ { "name": "pdf-tools", - "version": "0.2.0", + "version": "0.2.1", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", - "lint": "echo \"no lint configured\"" + "typecheck": "tsc --noEmit", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run --environment jsdom", + "test:watch": "vitest --environment jsdom", + "check": "npm run typecheck && npm run lint && npm run test && npm run build" }, "dependencies": { "pdf-lib": "^1.17.1", @@ -15,11 +22,24 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.8.0", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.2", - "@vitejs/plugin-react-swc": "^3.7.0", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "prettier": "^3.8.3", "typescript": "^5.6.3", - "vite": "^5.4.10" + "typescript-eslint": "^8.59.3", + "vite": "^8.0.13", + "vitest": "^4.1.6" }, "description": "Browser-only, self-hostable PDF workbench for page-level PDF operations." } diff --git a/pdf-tools.zip b/pdf-tools.zip new file mode 100644 index 0000000..b93bc3e Binary files /dev/null and b/pdf-tools.zip differ diff --git a/prettierignore.txt b/prettierignore.txt new file mode 100644 index 0000000..8336eb0 --- /dev/null +++ b/prettierignore.txt @@ -0,0 +1,4 @@ +node_modules +coverage +dist +*.zip diff --git a/prettierrc.json b/prettierrc.json new file mode 100644 index 0000000..94e30b6 --- /dev/null +++ b/prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "es5", + "printWidth": 80 +} diff --git a/src/App.tsx b/src/App.tsx index 9acd01d..24e7ae9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,92 +1,42 @@ -import React, { useEffect, useRef, useState } from 'react'; -import Layout from './components/Layout'; -import FileLoader from './components/FileLoader'; -import ReorderPanel from './components/ReorderPanel'; -import ActionsPanel from './components/ActionsPanel'; -import PagePreviewModal from './components/PagePreviewModal'; -import WorkspacePanel from './components/WorkspacePanel'; +import React, { useCallback, useEffect, useState } from "react"; +import Layout from "./components/Layout"; +import FileLoader from "./components/FileLoader"; +import ReorderPanel from "./components/ReorderPanel"; +import ActionsPanel from "./components/ActionsPanel"; +import PagePreviewModal from "./components/PagePreviewModal"; +import WorkspacePanel from "./components/WorkspacePanel"; import ActionDialog, { type ActionDialogAction, -} from './components/ActionDialog'; -import HelpDialog from './components/HelpDialog'; -import { PDFDocument } from 'pdf-lib'; +} from "./components/ActionDialog"; +import HelpDialog from "./components/HelpDialog"; +import { PDFDocument } from "pdf-lib"; import type { StoredWorkspace, WorkspaceSummary, -} from './workspace/workspaceTypes'; -import type { - WorkspaceCommand, - WorkspaceCommandRecord, - WorkspaceCommandState, -} from './workspace/workspaceCommands'; +} from "./workspace/workspaceTypes"; import { - createSnapshotCommand, - reviveWorkspaceCommand, - toWorkspaceCommandRecord, -} from './workspace/workspaceCommands'; + createInitialPageRefs, + createPageRefId, + createWorkspaceId, + defaultWorkspaceNameFromPdfName, + normalizeRotation, + useWorkspaceState, +} from "./workspace/useWorkspaceState"; import { deleteWorkspaceFromIndexedDb, listWorkspaces, loadWorkspaceFromIndexedDb, saveWorkspaceToIndexedDb, -} from './workspace/workspaceDb'; -import type { PageRef, PdfFile, SplitResult } from './pdf/pdfTypes'; +} from "./workspace/workspaceDb"; +import type { PageRef, PdfFile } from "./pdf/pdfTypes"; import { loadPdfFromFile, mergePdfFiles, splitIntoSinglePages, exportPages, -} from './pdf/pdfService'; -import { - generateThumbnailsProgressive, - generateThumbnailsWithRotationsProgressive, -} from './pdf/pdfThumbnailService'; - -const THUMBNAIL_MAX_HEIGHT = 150; -const THUMBNAIL_MAX_WIDTH = 140; -const THUMBNAIL_CONCURRENCY = 3; - -function createId(prefix: string): string { - if (typeof crypto !== 'undefined' && crypto.randomUUID) { - return crypto.randomUUID(); - } - - return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`; -} - -function defaultWorkspaceNameFromPdfName(pdfName: string): string { - return pdfName.replace(/\.pdf$/i, '') || 'Untitled workspace'; -} - -function createPageRefId(): string { - return createId('page'); -} - -function createInitialPageRefs(pageCount: number): PageRef[] { - return Array.from({ length: pageCount }, (_, sourcePageIndex) => ({ - id: createPageRefId(), - sourcePageIndex, - rotation: 0, - })); -} - -function normalizeRotation(rotation: number | undefined): number { - return (((rotation ?? 0) % 360) + 360) % 360; -} - -function thumbnailCacheKey( - pdfId: string, - sourcePageIndex: number, - rotation: number -): string { - return [ - pdfId, - sourcePageIndex, - normalizeRotation(rotation), - THUMBNAIL_MAX_WIDTH, - THUMBNAIL_MAX_HEIGHT, - ].join(':'); -} +} from "./pdf/pdfService"; +import { usePdfThumbnails } from "./pdf/usePdfThumbnails"; +import { usePdfGeneratedOutputs } from "./hooks/usePdfGeneratedOutputs"; function isEditableKeyboardTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; @@ -94,9 +44,9 @@ function isEditableKeyboardTarget(target: EventTarget | null): boolean { const tagName = target.tagName.toLowerCase(); return ( target.isContentEditable || - tagName === 'input' || - tagName === 'textarea' || - tagName === 'select' + tagName === "input" || + tagName === "textarea" || + tagName === "select" ); } @@ -113,49 +63,69 @@ const App: React.FC = () => { const [error, setError] = useState(null); const [workspaces, setWorkspaces] = useState([]); - const [activeWorkspaceId, setActiveWorkspaceId] = useState(null); - const [workspaceName, setWorkspaceName] = useState(''); - const [workspaceDirty, setWorkspaceDirty] = useState(false); - const [workspaceMessage, setWorkspaceMessage] = useState(null); - const [workspaceHistory, setWorkspaceHistory] = useState([]); - const [redoHistory, setRedoHistory] = useState([]); - - const [pages, setPages] = useState([]); - const [reorderThumbnails, setReorderThumbnails] = useState>({}); - - const [selectedPageIds, setSelectedPageIds] = useState([]); - const [lastSelectedVisualIndex, setLastSelectedVisualIndex] = useState(null); - - const [splitResults, setSplitResults] = useState([]); - const [subsetUrl, setSubsetUrl] = useState(null); - const [subsetFilename, setSubsetFilename] = useState(null); - const [exportUrl, setExportUrl] = useState(null); - const [exportFilename, setExportFilename] = useState(null); + const [activeWorkspaceId, setActiveWorkspaceId] = useState( + null, + ); + const [workspaceName, setWorkspaceName] = useState(""); const [previewPageId, setPreviewPageId] = useState(null); const [pendingFile, setPendingFile] = useState(null); const [showMergeOptions, setShowMergeOptions] = useState(false); - const [mergeMode, setMergeMode] = useState<'overwrite' | 'append' | 'insertAt'>('append'); - const [mergeInsertAt, setMergeInsertAt] = useState(''); + const [mergeMode, setMergeMode] = useState< + "overwrite" | "append" | "insertAt" + >("append"); + const [mergeInsertAt, setMergeInsertAt] = useState(""); - const thumbnailCacheRef = useRef>(new Map()); - const latestPagesRef = useRef([]); - const previousPageRotationsRef = useRef>(new Map()); + const { + splitDownloads, + subsetDownload, + exportDownload, + replaceSplitResults, + replaceSubsetResult, + replaceExportResult, + clearAllResults: clearGeneratedOutputs, + } = usePdfGeneratedOutputs(); - const resetGeneratedUrls = () => { - if (subsetUrl) { - URL.revokeObjectURL(subsetUrl); - setSubsetUrl(null); - setSubsetFilename(null); - } + const handleWorkspaceContentChanged = useCallback(() => { + clearGeneratedOutputs(); + }, [clearGeneratedOutputs]); - if (exportUrl) { - URL.revokeObjectURL(exportUrl); - setExportUrl(null); - setExportFilename(null); - } - }; + const { + pages, + selectedPageIds, + setSelectedPageIds, + lastSelectedVisualIndex, + setLastSelectedVisualIndex, + workspaceDirty, + setWorkspaceDirty, + workspaceMessage, + setWorkspaceMessage, + workspaceHistory, + redoHistory, + getCurrentCommandState, + createWorkspaceCommand, + executeWorkspaceCommand, + handleUndo, + handleRedo, + replaceWorkspaceState, + resetWorkspaceState: resetWorkspaceCommandState, + } = useWorkspaceState({ onContentChanged: handleWorkspaceContentChanged }); + + const handleThumbnailError = useCallback( + (message: string, thrown: unknown) => { + console.error(thrown); + setError(message); + }, + [], + ); + + const { thumbnails: reorderThumbnails, clearThumbnailCache } = + usePdfThumbnails({ + pdf, + pages, + onError: handleThumbnailError, + }); const refreshWorkspaces = async () => { try { @@ -163,7 +133,7 @@ const App: React.FC = () => { setWorkspaces(summaries); } catch (e) { console.error(e); - setError('Failed to read saved workspaces from browser storage.'); + setError("Failed to read saved workspaces from browser storage."); } }; @@ -174,93 +144,11 @@ const App: React.FC = () => { const resetWorkspaceState = () => { setPdf(null); setActiveWorkspaceId(null); - setWorkspaceName(''); - setWorkspaceDirty(false); - setWorkspaceMessage(null); - setWorkspaceHistory([]); - setSplitResults([]); - setSelectedPageIds([]); - setLastSelectedVisualIndex(null); - resetGeneratedUrls(); - setReorderThumbnails({}); - thumbnailCacheRef.current.clear(); - previousPageRotationsRef.current.clear(); - latestPagesRef.current = []; - setPages([]); + setWorkspaceName(""); + resetWorkspaceCommandState(); + clearGeneratedOutputs(); + clearThumbnailCache(); setPreviewPageId(null); - setWorkspaceHistory([]); - setRedoHistory([]); - }; - - const getCurrentCommandState = (): WorkspaceCommandState => ({ - pages, - selectedPageIds, - lastSelectedVisualIndex, - }); - - const applyCommandState = (state: WorkspaceCommandState) => { - setPages(state.pages); - latestPagesRef.current = state.pages; - setSelectedPageIds(state.selectedPageIds); - setLastSelectedVisualIndex(state.lastSelectedVisualIndex); - }; - - const invalidateWorkspaceOutputs = () => { - setSplitResults([]); - resetGeneratedUrls(); - setWorkspaceDirty(true); - setWorkspaceMessage(null); - }; - - const executeWorkspaceCommand = (command: WorkspaceCommand) => { - const nextState = command.do(getCurrentCommandState()); - - applyCommandState(nextState); - setWorkspaceHistory((prev) => [...prev, toWorkspaceCommandRecord(command)]); - setRedoHistory([]); - invalidateWorkspaceOutputs(); - }; - - const createWorkspaceCommand = (params: { - type: string; - label: string; - before: WorkspaceCommandState; - after: WorkspaceCommandState; - details?: Record; - }): WorkspaceCommand => - createSnapshotCommand({ - id: createId('command'), - type: params.type, - label: params.label, - before: params.before, - after: params.after, - details: params.details, - }); - - const handleUndo = () => { - const record = workspaceHistory[workspaceHistory.length - 1]; - if (!record) return; - - const command = reviveWorkspaceCommand(record); - const previousState = command.undo(getCurrentCommandState()); - - applyCommandState(previousState); - setWorkspaceHistory((prev) => prev.slice(0, -1)); - setRedoHistory((prev) => [...prev, record]); - invalidateWorkspaceOutputs(); - }; - - const handleRedo = () => { - const record = redoHistory[redoHistory.length - 1]; - if (!record) return; - - const command = reviveWorkspaceCommand(record); - const nextState = command.do(getCurrentCommandState()); - - applyCommandState(nextState); - setRedoHistory((prev) => prev.slice(0, -1)); - setWorkspaceHistory((prev) => [...prev, record]); - invalidateWorkspaceOutputs(); }; const handleSaveWorkspace = async (): Promise => { @@ -269,10 +157,13 @@ const App: React.FC = () => { setError(null); const now = new Date().toISOString(); - const name = workspaceName.trim() || defaultWorkspaceNameFromPdfName(pdf.name); - const workspaceId = activeWorkspaceId ?? createId('workspace'); + const name = + workspaceName.trim() || defaultWorkspaceNameFromPdfName(pdf.name); + const workspaceId = activeWorkspaceId ?? createWorkspaceId(); - const existing = workspaces.find((workspace) => workspace.id === workspaceId); + const existing = workspaces.find( + (workspace) => workspace.id === workspaceId, + ); const workspace: StoredWorkspace = { schemaVersion: 1, @@ -309,7 +200,7 @@ const App: React.FC = () => { } catch (e) { console.error(e); setError( - 'Failed to save workspace. The browser storage quota may be full.' + "Failed to save workspace. The browser storage quota may be full.", ); return false; } finally { @@ -330,12 +221,10 @@ const App: React.FC = () => { } openActionDialog({ - title: 'Reset workspace?', + title: "Reset workspace?", content: ( <> -

- This workspace has unsaved changes. -

+

This workspace has unsaved changes.

Do you want to save it before resetting?

@@ -343,21 +232,21 @@ const App: React.FC = () => { ), actions: [ { - label: 'Cancel', - variant: 'secondary', + label: "Cancel", + variant: "secondary", onClick: closeActionDialog, }, { - label: 'Reset without saving', - variant: 'danger', + label: "Reset without saving", + variant: "danger", onClick: () => { closeActionDialog(); performResetWorkspace(); }, }, { - label: 'Save and reset', - variant: 'primary', + label: "Save and reset", + variant: "primary", autoFocus: true, onClick: async () => { closeActionDialog(); @@ -379,12 +268,12 @@ const App: React.FC = () => { const loaded = await loadWorkspaceFromIndexedDb(workspaceId); if (!loaded) { - setError('Workspace not found.'); + setError("Workspace not found."); await refreshWorkspaces(); return; } - resetGeneratedUrls(); + clearGeneratedOutputs(); const doc = await PDFDocument.load(loaded.pdfArrayBuffer); @@ -397,27 +286,24 @@ const App: React.FC = () => { }; setPdf(loadedPdf); - setPages(loaded.workspace.pages); - latestPagesRef.current = loaded.workspace.pages; - - setSelectedPageIds(loaded.workspace.selectedPageIds ?? []); - setLastSelectedVisualIndex(null); - setSplitResults([]); + replaceWorkspaceState({ + pages: loaded.workspace.pages, + selectedPageIds: loaded.workspace.selectedPageIds ?? [], + lastSelectedVisualIndex: null, + history: loaded.workspace.history ?? [], + redoHistory: loaded.workspace.redoHistory ?? [], + dirty: false, + message: `Workspace "${loaded.workspace.name}" loaded.`, + }); setPreviewPageId(null); - setReorderThumbnails({}); - thumbnailCacheRef.current.clear(); - previousPageRotationsRef.current.clear(); + clearThumbnailCache(); setActiveWorkspaceId(loaded.workspace.id); setWorkspaceName(loaded.workspace.name); - setWorkspaceHistory(loaded.workspace.history ?? []); - setRedoHistory(loaded.workspace.redoHistory ?? []); - setWorkspaceDirty(false); - setWorkspaceMessage(`Workspace "${loaded.workspace.name}" loaded.`); } catch (e) { console.error(e); - setError('Failed to load workspace from browser storage.'); + setError("Failed to load workspace from browser storage."); } finally { setIsBusy(false); } @@ -425,30 +311,31 @@ const App: React.FC = () => { const handleDeleteWorkspace = (workspaceId: string) => { const workspace = workspaces.find((item) => item.id === workspaceId); - const name = workspace?.name ?? 'this workspace'; + const name = workspace?.name ?? "this workspace"; openActionDialog({ - title: 'Delete workspace?', + title: "Delete workspace?", content: ( <>

- Delete the saved workspace {name} from this browser? + Delete the saved workspace {name} from this + browser?

- The currently open in-memory document will not be closed, but the saved - workspace entry will be removed. + The currently open in-memory document will not be closed, but the + saved workspace entry will be removed.

), actions: [ { - label: 'Cancel', - variant: 'secondary', + label: "Cancel", + variant: "secondary", onClick: closeActionDialog, }, { - label: 'Delete workspace', - variant: 'danger', + label: "Delete workspace", + variant: "danger", autoFocus: true, onClick: () => { closeActionDialog(); @@ -469,14 +356,14 @@ const App: React.FC = () => { setActiveWorkspaceId(null); setWorkspaceDirty(true); setWorkspaceMessage( - 'Saved workspace deleted. Current in-memory document remains open.' + "Saved workspace deleted. Current in-memory document remains open.", ); } await refreshWorkspaces(); } catch (e) { console.error(e); - setError('Failed to delete workspace.'); + setError("Failed to delete workspace."); } }; @@ -490,17 +377,20 @@ const App: React.FC = () => { const initialPages = createInitialPageRefs(loaded.pageCount); setPdf(loaded); - setPages(initialPages); - latestPagesRef.current = initialPages; + replaceWorkspaceState({ + pages: initialPages, + selectedPageIds: [], + lastSelectedVisualIndex: null, + history: [], + redoHistory: [], + dirty: true, + message: null, + }); setWorkspaceName(defaultWorkspaceNameFromPdfName(loaded.name)); - setWorkspaceHistory([]); - setRedoHistory([]); - setWorkspaceDirty(true); - setWorkspaceMessage(null); } catch (e) { console.error(e); - setError('Failed to load PDF (see console).'); + setError("Failed to load PDF (see console)."); } finally { setIsBusy(false); } @@ -512,7 +402,7 @@ const App: React.FC = () => { } else { setPendingFile(file); setShowMergeOptions(true); - setMergeMode('append'); + setMergeMode("append"); setMergeInsertAt(String(pages.length + 1)); } }; @@ -525,7 +415,7 @@ const App: React.FC = () => { const handleMergeConfirm = async () => { if (!pendingFile) return; - if (!pdf || mergeMode === 'overwrite') { + if (!pdf || mergeMode === "overwrite") { await loadFileAsNew(pendingFile); setPendingFile(null); setShowMergeOptions(false); @@ -553,12 +443,12 @@ const App: React.FC = () => { // 3) Determine insert position (0-based) let insertAt = pages.length; // default: append at end - if (mergeMode === 'insertAt') { + if (mergeMode === "insertAt") { const parsed = parseInt(mergeInsertAt, 10); if (Number.isFinite(parsed)) { insertAt = Math.min(Math.max(parsed - 1, 0), pages.length); } - } else if (mergeMode === 'append') { + } else if (mergeMode === "append") { insertAt = pages.length; } @@ -568,26 +458,23 @@ const App: React.FC = () => { // 5) Reset state to the merged document setPdf(mergedPdf); - - setPages(mergedPages); - latestPagesRef.current = mergedPages; - setSelectedPageIds([]); - setLastSelectedVisualIndex(null); - setSplitResults([]); - resetGeneratedUrls(); - setReorderThumbnails({}); - thumbnailCacheRef.current.clear(); - previousPageRotationsRef.current.clear(); + replaceWorkspaceState({ + pages: mergedPages, + selectedPageIds: [], + lastSelectedVisualIndex: null, + history: [], + redoHistory: [], + dirty: true, + message: null, + }); + clearGeneratedOutputs(); + clearThumbnailCache(); setPreviewPageId(null); setWorkspaceName(defaultWorkspaceNameFromPdfName(mergedPdf.name)); - setWorkspaceHistory([]); - setRedoHistory([]); - setWorkspaceDirty(true); setActiveWorkspaceId(null); - setWorkspaceMessage(null); } catch (e) { console.error(e); - setError('Failed to merge PDF (see console).'); + setError("Failed to merge PDF (see console)."); } finally { setIsBusy(false); setPendingFile(null); @@ -596,191 +483,10 @@ const App: React.FC = () => { }; useEffect(() => { - latestPagesRef.current = pages; - }, [pages]); - - useEffect(() => { - if (!pdf) { - setReorderThumbnails({}); - thumbnailCacheRef.current.clear(); - previousPageRotationsRef.current.clear(); - return; - } - - const controller = new AbortController(); - - latestPagesRef.current = pages; - thumbnailCacheRef.current.clear(); - previousPageRotationsRef.current = new Map( - pages.map((page) => [page.id, normalizeRotation(page.rotation)]) - ); - setReorderThumbnails({}); - - void generateThumbnailsProgressive(pdf.arrayBuffer, { - maxHeight: THUMBNAIL_MAX_HEIGHT, - maxWidth: THUMBNAIL_MAX_WIDTH, - concurrency: THUMBNAIL_CONCURRENCY, - signal: controller.signal, - onThumbnail: ({ pageIndex, dataUrl }) => { - if (controller.signal.aborted) return; - - thumbnailCacheRef.current.set( - thumbnailCacheKey(pdf.id, pageIndex, 0), - dataUrl - ); - - const currentPages = latestPagesRef.current; - const updates: Record = {}; - - for (const page of currentPages) { - if ( - page.sourcePageIndex === pageIndex && - normalizeRotation(page.rotation) === 0 - ) { - updates[page.id] = dataUrl; - } - } - - if (Object.keys(updates).length === 0) return; - - setReorderThumbnails((prev) => ({ - ...prev, - ...updates, - })); - }, - }).catch((e) => { - if (!controller.signal.aborted) { - console.error(e); - setError('Failed to generate thumbnails (see console).'); - } - }); - - return () => { - controller.abort(); - }; - }, [pdf]); - - useEffect(() => { - if (!pdf) { - previousPageRotationsRef.current.clear(); - return; - } - - const previousRotations = previousPageRotationsRef.current; - const changedPages = pages.filter( - (page) => - normalizeRotation(previousRotations.get(page.id)) !== - normalizeRotation(page.rotation) - ); - - previousPageRotationsRef.current = new Map( - pages.map((page) => [page.id, normalizeRotation(page.rotation)]) - ); - - if (changedPages.length === 0) return; - - const cachedUpdates: Record = {}; - const pagesToRender: PageRef[] = []; - - for (const page of changedPages) { - const rotation = normalizeRotation(page.rotation); - const cached = thumbnailCacheRef.current.get( - thumbnailCacheKey(pdf.id, page.sourcePageIndex, rotation) - ); - - if (cached) { - cachedUpdates[page.id] = cached; - } else { - pagesToRender.push(page); - } - } - - if (Object.keys(cachedUpdates).length > 0) { - setReorderThumbnails((prev) => ({ - ...prev, - ...cachedUpdates, - })); - } - - if (pagesToRender.length === 0) return; - - const controller = new AbortController(); - const groups = new Map(); - - for (const page of pagesToRender) { - const rotation = normalizeRotation(page.rotation); - const group = groups.get(rotation) ?? []; - group.push(page); - groups.set(rotation, group); - } - - const renderGroups = async () => { - for (const [rotation, groupPages] of groups) { - if (controller.signal.aborted) return; - - const pageIndices = Array.from( - new Set(groupPages.map((page) => page.sourcePageIndex)) - ); - const rotationsBySourcePage: Record = {}; - - for (const pageIndex of pageIndices) { - rotationsBySourcePage[pageIndex] = rotation; - } - - await generateThumbnailsWithRotationsProgressive( - pdf.arrayBuffer, - rotationsBySourcePage, - { - maxHeight: THUMBNAIL_MAX_HEIGHT, - maxWidth: THUMBNAIL_MAX_WIDTH, - concurrency: Math.min(THUMBNAIL_CONCURRENCY, pageIndices.length), - pageIndices, - signal: controller.signal, - onThumbnail: ({ pageIndex, dataUrl }) => { - if (controller.signal.aborted) return; - - thumbnailCacheRef.current.set( - thumbnailCacheKey(pdf.id, pageIndex, rotation), - dataUrl - ); - - const updates: Record = {}; - - for (const page of latestPagesRef.current) { - if ( - page.sourcePageIndex === pageIndex && - normalizeRotation(page.rotation) === rotation - ) { - updates[page.id] = dataUrl; - } - } - - if (Object.keys(updates).length === 0) return; - - setReorderThumbnails((prev) => ({ - ...prev, - ...updates, - })); - }, - } - ); - } - }; - - void renderGroups().catch((e) => { - if (!controller.signal.aborted) { - console.error(e); - setError('Failed to generate rotated thumbnails (see console).'); - } - }); - - return () => { - controller.abort(); - }; - }, [pdf, pages]); - - useEffect(() => { - if (previewPageId != null && !pages.some((page) => page.id === previewPageId)) { + if ( + previewPageId != null && + !pages.some((page) => page.id === previewPageId) + ) { setPreviewPageId(null); } }, [previewPageId, pages]); @@ -791,16 +497,16 @@ const App: React.FC = () => { const handleKeyDown = (e: KeyboardEvent) => { if (isEditableKeyboardTarget(e.target)) return; - if (e.key === 'F1' || e.key === '?') { + if (e.key === "F1" || e.key === "?") { e.preventDefault(); setHelpOpen(true); } }; - window.addEventListener('keydown', handleKeyDown); + window.addEventListener("keydown", handleKeyDown); return () => { - window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener("keydown", handleKeyDown); }; }, []); @@ -810,13 +516,13 @@ const App: React.FC = () => { const afterPages = pages.map((page) => page.id === pageId ? { ...page, rotation: (normalizeRotation(page.rotation) + 90) % 360 } - : page + : page, ); executeWorkspaceCommand( createWorkspaceCommand({ - type: 'page.rotate', - label: 'Rotated page clockwise', + type: "page.rotate", + label: "Rotated page clockwise", before, after: { ...before, @@ -826,7 +532,7 @@ const App: React.FC = () => { pageId, degrees: 90, }, - }) + }), ); }; @@ -835,13 +541,13 @@ const App: React.FC = () => { const afterPages = pages.map((page) => page.id === pageId ? { ...page, rotation: (normalizeRotation(page.rotation) + 270) % 360 } - : page + : page, ); executeWorkspaceCommand( createWorkspaceCommand({ - type: 'page.rotate', - label: 'Rotated page counterclockwise', + type: "page.rotate", + label: "Rotated page counterclockwise", before, after: { ...before, @@ -851,7 +557,7 @@ const App: React.FC = () => { pageId, degrees: -90, }, - }) + }), ); }; @@ -859,10 +565,10 @@ const App: React.FC = () => { const page = pages.find((item) => item.id === pageId); const visualIndex = page ? pages.indexOf(page) : -1; const pageLabel = - visualIndex >= 0 ? `page at position ${visualIndex + 1}` : 'this page'; + visualIndex >= 0 ? `page at position ${visualIndex + 1}` : "this page"; openActionDialog({ - title: 'Delete page?', + title: "Delete page?", content: (

Delete {pageLabel} from the current workspace? @@ -870,13 +576,13 @@ const App: React.FC = () => { ), actions: [ { - label: 'Cancel', - variant: 'secondary', + label: "Cancel", + variant: "secondary", onClick: closeActionDialog, }, { - label: 'Delete page', - variant: 'danger', + label: "Delete page", + variant: "danger", autoFocus: true, onClick: () => { closeActionDialog(); @@ -892,8 +598,8 @@ const App: React.FC = () => { executeWorkspaceCommand( createWorkspaceCommand({ - type: 'page.delete', - label: 'Deleted page', + type: "page.delete", + label: "Deleted page", before, after: { pages: pages.filter((page) => page.id !== pageId), @@ -903,7 +609,7 @@ const App: React.FC = () => { details: { pageId, }, - }) + }), ); }; @@ -912,8 +618,8 @@ const App: React.FC = () => { executeWorkspaceCommand( createWorkspaceCommand({ - type: 'pages.reorder', - label: 'Reordered pages', + type: "pages.reorder", + label: "Reordered pages", before, after: { ...before, @@ -922,14 +628,14 @@ const App: React.FC = () => { details: { pageCount: newPages.length, }, - }) + }), ); }; const handleToggleSelect = ( pageId: string, visualIndex: number, - e: React.MouseEvent + e: React.MouseEvent, ) => { setSelectedPageIds((prev) => { if (e.shiftKey && lastSelectedVisualIndex !== null && pages.length > 0) { @@ -961,7 +667,7 @@ const App: React.FC = () => { setSelectedPageIds([]); setLastSelectedVisualIndex(null); }; - + const handleDeleteSelected = () => { if (selectedPageIds.length === 0) return; @@ -970,29 +676,28 @@ const App: React.FC = () => { openActionDialog({ title: idsToDelete.length === 1 - ? 'Delete selected page?' - : 'Delete selected pages?', + ? "Delete selected page?" + : "Delete selected pages?", content: (

- Delete{' '} + Delete{" "} {idsToDelete.length === 1 - ? '1 selected page' + ? "1 selected page" : `${idsToDelete.length} selected pages`} - {' '} + {" "} from the current workspace?

), actions: [ { - label: 'Cancel', - variant: 'secondary', + label: "Cancel", + variant: "secondary", onClick: closeActionDialog, }, { - label: - idsToDelete.length === 1 ? 'Delete page' : 'Delete pages', - variant: 'danger', + label: idsToDelete.length === 1 ? "Delete page" : "Delete pages", + variant: "danger", autoFocus: true, onClick: () => { closeActionDialog(); @@ -1011,10 +716,10 @@ const App: React.FC = () => { executeWorkspaceCommand( createWorkspaceCommand({ - type: 'pages.delete', + type: "pages.delete", label: pageIdsToDelete.length === 1 - ? 'Deleted selected page' + ? "Deleted selected page" : `Deleted ${pageIdsToDelete.length} selected pages`, before, after: { @@ -1025,7 +730,7 @@ const App: React.FC = () => { details: { count: pageIdsToDelete.length, }, - }) + }), ); }; @@ -1052,34 +757,14 @@ const App: React.FC = () => { ...pages.slice(clampedSlot), ]; - const thumbnailUpdates: Record = {}; - - sourcePages.forEach((sourcePage, index) => { - const copiedPage = copiedPages[index]; - - const thumbnail = - reorderThumbnails[sourcePage.id] ?? - thumbnailCacheRef.current.get( - thumbnailCacheKey( - pdf.id, - sourcePage.sourcePageIndex, - sourcePage.rotation - ) - ); - - if (thumbnail) { - thumbnailUpdates[copiedPage.id] = thumbnail; - } - }); - const before = getCurrentCommandState(); executeWorkspaceCommand( createWorkspaceCommand({ - type: 'pages.copy', + type: "pages.copy", label: copiedPages.length === 1 - ? 'Copied page' + ? "Copied page" : `Copied ${copiedPages.length} pages`, before, after: { @@ -1091,15 +776,8 @@ const App: React.FC = () => { count: copiedPages.length, insertSlot: clampedSlot, }, - }) + }), ); - - if (Object.keys(thumbnailUpdates).length > 0) { - setReorderThumbnails((prev) => ({ - ...prev, - ...thumbnailUpdates, - })); - } }; const closeActionDialog = () => { @@ -1152,7 +830,7 @@ const App: React.FC = () => { const key = e.key.toLowerCase(); - if ((e.ctrlKey || e.metaKey) && key === 'z') { + if ((e.ctrlKey || e.metaKey) && key === "z") { e.preventDefault(); if (e.shiftKey) { handleRedo(); @@ -1162,38 +840,53 @@ const App: React.FC = () => { return; } - if ((e.ctrlKey || e.metaKey) && key === 'y') { + if ((e.ctrlKey || e.metaKey) && key === "y") { e.preventDefault(); handleRedo(); return; } - if ((e.ctrlKey || e.metaKey) && key === 'a') { + if ((e.ctrlKey || e.metaKey) && key === "a") { e.preventDefault(); setSelectedPageIds(pages.map((page) => page.id)); setLastSelectedVisualIndex(null); return; } - if ((e.key === 'Delete' || e.key === 'Backspace') && selectedPageIds.length > 0) { + if ( + (e.key === "Delete" || e.key === "Backspace") && + selectedPageIds.length > 0 + ) { e.preventDefault(); handleDeleteSelected(); return; } - if (e.key === 'Escape' && selectedPageIds.length > 0) { + if (e.key === "Escape" && selectedPageIds.length > 0) { e.preventDefault(); setSelectedPageIds([]); setLastSelectedVisualIndex(null); } }; - window.addEventListener('keydown', handleKeyDown); + window.addEventListener("keydown", handleKeyDown); return () => { - window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener("keydown", handleKeyDown); }; - }, [hasPdf, previewPageId, pages, selectedPageIds, workspaceHistory, redoHistory]); + }, [ + hasPdf, + previewPageId, + pages, + selectedPageIds, + workspaceHistory, + redoHistory, + handleUndo, + handleRedo, + handleDeleteSelected, + setSelectedPageIds, + setLastSelectedVisualIndex, + ]); const handleSplit = async () => { if (!pdf) return; @@ -1201,38 +894,30 @@ const App: React.FC = () => { setIsBusy(true); try { const result = await splitIntoSinglePages(pdf); - setSplitResults(result); + replaceSplitResults(result); } catch (e) { console.error(e); - setError('Error while splitting PDF (see console).'); + setError("Error while splitting PDF (see console)."); } finally { setIsBusy(false); } }; const handleExtractSelected = async () => { - if (!pdf || selectedPageIds.length === 0) return + if (!pdf || selectedPageIds.length === 0) return; setError(null); setIsBusy(true); - if (subsetUrl) { - URL.revokeObjectURL(subsetUrl); - setSubsetUrl(null); - setSubsetFilename(null); - } - try { const selectedSet = new Set(selectedPageIds); const selectedPages = pages.filter((page) => selectedSet.has(page.id)); const blob = await exportPages(pdf, selectedPages); - const url = URL.createObjectURL(blob); - const base = pdf.name.replace(/\.pdf$/i, ''); + const base = pdf.name.replace(/\.pdf$/i, ""); const filename = `${base}_selected.pdf`; - setSubsetUrl(url); - setSubsetFilename(filename); + replaceSubsetResult(blob, filename); } catch (e) { console.error(e); - setError('Error while extracting selected pages (see console).'); + setError("Error while extracting selected pages (see console)."); } finally { setIsBusy(false); } @@ -1243,22 +928,14 @@ const App: React.FC = () => { setError(null); setIsBusy(true); - if (exportUrl) { - URL.revokeObjectURL(exportUrl); - setExportUrl(null); - setExportFilename(null); - } - try { const blob = await exportPages(pdf, pages); - const url = URL.createObjectURL(blob); - const base = pdf.name.replace(/\.pdf$/i, ''); + const base = pdf.name.replace(/\.pdf$/i, ""); const filename = `${base}_reordered.pdf`; - setExportUrl(url); - setExportFilename(filename); + replaceExportResult(blob, filename); } catch (e) { console.error(e); - setError('Error while exporting reordered PDF (see console).'); + setError("Error while exporting reordered PDF (see console)."); } finally { setIsBusy(false); } @@ -1305,62 +982,62 @@ const App: React.FC = () => { {showMergeOptions && pendingFile && pdf && pages.length > 0 && (

Open file: merge or replace?

-

- You already have {pdf.name} with {pages.length}{' '} - pages open. What should happen with{' '} +

+ You already have {pdf.name} with {pages.length}{" "} + pages open. What should happen with{" "} {pendingFile.name}?

-
+
@@ -1419,7 +1096,6 @@ const App: React.FC = () => { onDeleteSelected={handleDeleteSelected} /> - { onSplit={handleSplit} onExtractSelected={handleExtractSelected} onExportReordered={handleExportReordered} - splitResults={splitResults} - subsetDownloadUrl={subsetUrl} - subsetFilename={subsetFilename} - exportDownloadUrl={exportUrl} - exportFilename={exportFilename} + splitDownloads={splitDownloads} + subsetDownload={subsetDownload} + exportDownload={exportDownload} /> {error && (
Error: {error}
@@ -1459,7 +1133,7 @@ const App: React.FC = () => { diff --git a/src/components/ActionDialog.tsx b/src/components/ActionDialog.tsx index b4a399c..bb50f8c 100644 --- a/src/components/ActionDialog.tsx +++ b/src/components/ActionDialog.tsx @@ -1,9 +1,9 @@ -import React, { useEffect } from 'react'; +import React, { useEffect } from "react"; export interface ActionDialogAction { label: string; onClick: () => void | Promise; - variant?: 'primary' | 'secondary' | 'danger'; + variant?: "primary" | "secondary" | "danger"; disabled?: boolean; autoFocus?: boolean; title?: string; @@ -18,21 +18,21 @@ interface ActionDialogProps { } const backgroundByVariant: Record< - NonNullable, + NonNullable, string > = { - primary: '#2563eb', - secondary: '#e5e7eb', - danger: '#dc2626', + primary: "#2563eb", + secondary: "#e5e7eb", + danger: "#dc2626", }; const colorByVariant: Record< - NonNullable, + NonNullable, string > = { - primary: 'white', - secondary: '#111827', - danger: 'white', + primary: "white", + secondary: "#111827", + danger: "white", }; const ActionDialog: React.FC = ({ @@ -46,16 +46,16 @@ const ActionDialog: React.FC = ({ if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { + if (e.key === "Escape") { e.preventDefault(); onClose(); } }; - window.addEventListener('keydown', handleKeyDown); + window.addEventListener("keydown", handleKeyDown); return () => { - window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener("keydown", handleKeyDown); }; }, [open, onClose]); @@ -72,42 +72,42 @@ const ActionDialog: React.FC = ({ } }} style={{ - position: 'fixed', + position: "fixed", inset: 0, zIndex: 70, - background: 'rgba(15, 23, 42, 0.55)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: '1rem', + background: "rgba(15, 23, 42, 0.55)", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "1rem", }} >

{title} @@ -117,18 +117,18 @@ const ActionDialog: React.FC = ({ type="button" onClick={onClose} style={{ - border: 'none', - borderRadius: '999px', - width: '1.8rem', - height: '1.8rem', - background: '#e5e7eb', - color: '#111827', - cursor: 'pointer', - fontSize: '1.1rem', + border: "none", + borderRadius: "999px", + width: "1.8rem", + height: "1.8rem", + background: "#e5e7eb", + color: "#111827", + cursor: "pointer", + fontSize: "1.1rem", lineHeight: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', + display: "flex", + alignItems: "center", + justifyContent: "center", }} aria-label="Close dialog" > @@ -138,8 +138,8 @@ const ActionDialog: React.FC = ({
@@ -148,15 +148,15 @@ const ActionDialog: React.FC = ({
{actions.map((action) => { - const variant = action.variant ?? 'secondary'; + const variant = action.variant ?? "secondary"; return ( @@ -63,11 +62,11 @@ const ActionsPanel: React.FC = ({ className="secondary" disabled={disabled || selectedCount === 0} onClick={handleExtractSelectedClick} - style={{ flex: '1 1 45%' }} + style={{ flex: "1 1 45%" }} title={ selectedCount === 0 - ? 'Select at least one page' - : 'Create a PDF from selected pages' + ? "Select at least one page" + : "Create a PDF from selected pages" } > 📤 Extract selected ({selectedCount}) @@ -77,58 +76,52 @@ const ActionsPanel: React.FC = ({ className="secondary" disabled={disabled} onClick={onSplit} - style={{ flex: '1 1 45%' }} + style={{ flex: "1 1 45%" }} > 📂 Split into single PDFs
- {subsetDownloadUrl && subsetFilename && ( -
- Subset result:{' '} + {subsetDownload && ( + )} - {exportDownloadUrl && exportFilename && ( -
- Exported document:{' '} + {exportDownload && ( + )} - {splitResults.length > 0 && ( -
+ {splitDownloads.length > 0 && ( +
Single-page PDFs:
- {splitResults.map((r) => { - const url = URL.createObjectURL(r.blob); - return ( - { - setTimeout(() => URL.revokeObjectURL(url), 5000); - }} - > - {r.filename} - - ); - })} + {splitDownloads.map((download) => ( + + {download.filename} + + ))}
)} diff --git a/src/components/FileLoader.tsx b/src/components/FileLoader.tsx index 08d2af8..b18793e 100644 --- a/src/components/FileLoader.tsx +++ b/src/components/FileLoader.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import type { PdfFile } from '../pdf/pdfTypes'; +import React from "react"; +import type { PdfFile } from "../pdf/pdfTypes"; interface FileLoaderProps { pdf: PdfFile | null; @@ -11,7 +11,7 @@ const FileLoader: React.FC = ({ pdf, onFileLoaded }) => { const file = e.target.files?.[0]; if (file) { onFileLoaded(file); - e.target.value = ''; + e.target.value = ""; } }; @@ -22,7 +22,7 @@ const FileLoader: React.FC = ({ pdf, onFileLoaded }) => { {pdf && ( -
+
Loaded: {pdf.name}
diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index abbbad2..9e7afa7 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect } from "react"; interface HelpDialogProps { open: boolean; @@ -6,36 +6,51 @@ interface HelpDialogProps { } const shortcuts = [ - { keys: 'F1 / ?', description: 'Open this help and tutorial dialog' }, - { keys: 'Ctrl/⌘ + A', description: 'Select all pages in the current workspace' }, - { keys: 'Delete / Backspace', description: 'Delete the selected pages after confirmation' }, - { keys: 'Esc', description: 'Clear the page selection or close an open dialog' }, - { keys: 'Ctrl/⌘ + Z', description: 'Undo the latest workspace command' }, - { keys: 'Ctrl/⌘ + Shift + Z', description: 'Redo the next workspace command' }, - { keys: 'Ctrl/⌘ + Y', description: 'Redo the next workspace command' }, - { keys: '← / → in preview', description: 'Move to the previous or next page in the preview overlay' }, + { keys: "F1 / ?", description: "Open this help and tutorial dialog" }, + { + keys: "Ctrl/⌘ + A", + description: "Select all pages in the current workspace", + }, + { + keys: "Delete / Backspace", + description: "Delete the selected pages after confirmation", + }, + { + keys: "Esc", + description: "Clear the page selection or close an open dialog", + }, + { keys: "Ctrl/⌘ + Z", description: "Undo the latest workspace command" }, + { + keys: "Ctrl/⌘ + Shift + Z", + description: "Redo the next workspace command", + }, + { keys: "Ctrl/⌘ + Y", description: "Redo the next workspace command" }, + { + keys: "← / → in preview", + description: "Move to the previous or next page in the preview overlay", + }, ]; const tutorialSteps = [ { - title: '1. Open a PDF or load a workspace', - body: 'Start by selecting a local PDF file. If you saved workspaces before, you can restore one from browser storage instead.', + title: "1. Open a PDF or load a workspace", + body: "Start by selecting a local PDF file. If you saved workspaces before, you can restore one from browser storage instead.", }, { - title: '2. Arrange pages visually', - body: 'Drag page cards to reorder them. Rotate single pages, open the large preview with a click, or remove pages you do not want in the export.', + title: "2. Arrange pages visually", + body: "Drag page cards to reorder them. Rotate single pages, open the large preview with a click, or remove pages you do not want in the export.", }, { - title: '3. Select, copy, and delete pages', - body: 'Use the checkbox on a page card to select it. Shift-click extends a range. Dragging a selected page moves the whole selection; the copy controls duplicate selected pages into a chosen slot.', + title: "3. Select, copy, and delete pages", + body: "Use the checkbox on a page card to select it. Shift-click extends a range. Dragging a selected page moves the whole selection; the copy controls duplicate selected pages into a chosen slot.", }, { - title: '4. Save your workspace or export a PDF', - body: 'Saving a workspace keeps the current working state in this browser. Exporting creates a new PDF file for download.', + title: "4. Save your workspace or export a PDF", + body: "Saving a workspace keeps the current working state in this browser. Exporting creates a new PDF file for download.", }, { - title: '5. Use history deliberately', - body: 'Each workspace operation is stored as a command with label and timestamp. Undo and redo walk through that command history.', + title: "5. Use history deliberately", + body: "Each workspace operation is stored as a command with label and timestamp. Undo and redo walk through that command history.", }, ]; @@ -44,7 +59,7 @@ const HelpDialog: React.FC = ({ open, onClose }) => { if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key !== 'Escape') return; + if (e.key !== "Escape") return; e.preventDefault(); e.stopPropagation(); @@ -52,10 +67,10 @@ const HelpDialog: React.FC = ({ open, onClose }) => { onClose(); }; - window.addEventListener('keydown', handleKeyDown, { capture: true }); + window.addEventListener("keydown", handleKeyDown, { capture: true }); return () => { - window.removeEventListener('keydown', handleKeyDown, { capture: true }); + window.removeEventListener("keydown", handleKeyDown, { capture: true }); }; }, [open, onClose]); @@ -79,8 +94,8 @@ const HelpDialog: React.FC = ({ open, onClose }) => {

Help & tutorial

PDF Workbench is a browser-only page workspace. Use it to quickly - rearrange, split, merge, rotate, duplicate, and export PDFs without - uploading documents to a server. + rearrange, split, merge, rotate, duplicate, and export PDFs + without uploading documents to a server.

@@ -119,8 +134,8 @@ const HelpDialog: React.FC = ({ open, onClose }) => { ))}

- Shortcuts are ignored while typing in text fields or other editable - controls. + Shortcuts are ignored while typing in text fields or other + editable controls.

diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 5d51fe1..23e745e 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { APP_VERSION } from '../version'; +import React from "react"; +import { APP_VERSION } from "../version"; interface LayoutProps { children: React.ReactNode; @@ -44,4 +44,4 @@ const Layout: React.FC = ({ children, onOpenHelp }) => { ); }; -export default Layout; \ No newline at end of file +export default Layout; diff --git a/src/components/PagePreviewModal.tsx b/src/components/PagePreviewModal.tsx index 11417ee..2325467 100644 --- a/src/components/PagePreviewModal.tsx +++ b/src/components/PagePreviewModal.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useRef } from 'react'; -import type { PdfFile } from '../pdf/pdfTypes'; -import * as pdfjsLib from 'pdfjs-dist'; -import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url'; +import React, { useEffect, useRef } from "react"; +import type { PdfFile } from "../pdf/pdfTypes"; +import * as pdfjsLib from "pdfjs-dist"; +import pdfjsWorker from "pdfjs-dist/build/pdf.worker?worker&url"; // pdf.js worker setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -10,10 +10,10 @@ import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url'; interface PagePreviewModalProps { isOpen: boolean; pdf: PdfFile | null; - pageIndex: number | null; // original page index, 0-based - rotation: number; // degrees + pageIndex: number | null; // original page index, 0-based + rotation: number; // degrees - visualIndex: number | null; // current position in order, 0-based + visualIndex: number | null; // current position in order, 0-based totalPages: number; canGoPrevious: boolean; @@ -43,28 +43,28 @@ const PagePreviewModal: React.FC = ({ if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { + if (e.key === "Escape") { e.preventDefault(); onClose(); return; } - if (e.key === 'ArrowLeft' && canGoPrevious) { + if (e.key === "ArrowLeft" && canGoPrevious) { e.preventDefault(); onPrevious(); return; } - if (e.key === 'ArrowRight' && canGoNext) { + if (e.key === "ArrowRight" && canGoNext) { e.preventDefault(); onNext(); } }; - window.addEventListener('keydown', handleKeyDown); + window.addEventListener("keydown", handleKeyDown); return () => { - window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener("keydown", handleKeyDown); }; }, [isOpen, canGoPrevious, canGoNext, onPrevious, onNext, onClose]); @@ -77,7 +77,7 @@ const PagePreviewModal: React.FC = ({ try { const canvas = canvasRef.current; if (canvas) { - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (ctx) { ctx.clearRect(0, 0, canvas.width, canvas.height); } @@ -102,7 +102,7 @@ const PagePreviewModal: React.FC = ({ const scale = Math.min( maxWidth / viewport.width, - maxHeight / viewport.height + maxHeight / viewport.height, ); const scaledViewport = page.getViewport({ scale }); @@ -110,7 +110,7 @@ const PagePreviewModal: React.FC = ({ const visibleCanvas = canvasRef.current; if (!visibleCanvas) return; - const visibleCtx = visibleCanvas.getContext('2d'); + const visibleCtx = visibleCanvas.getContext("2d"); if (!visibleCtx) return; let canvasWidth = scaledViewport.width; @@ -126,8 +126,8 @@ const PagePreviewModal: React.FC = ({ visibleCanvas.width = canvasWidth; visibleCanvas.height = canvasHeight; - const baseCanvas = document.createElement('canvas'); - const baseCtx = baseCanvas.getContext('2d'); + const baseCanvas = document.createElement("canvas"); + const baseCtx = baseCanvas.getContext("2d"); if (!baseCtx) return; baseCanvas.width = scaledViewport.width; @@ -161,7 +161,7 @@ const PagePreviewModal: React.FC = ({ visibleCtx.drawImage(baseCanvas, 0, 0); visibleCtx.restore(); } catch (e) { - console.error('Error rendering preview', e); + console.error("Error rendering preview", e); } })(); @@ -181,30 +181,30 @@ const PagePreviewModal: React.FC = ({
e.stopPropagation()} style={{ - position: 'relative', - background: '#111827', - borderRadius: '0.75rem', - padding: '0.75rem', - maxWidth: '90vw', - maxHeight: '90vh', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '0.5rem', - overflow: 'visible', + position: "relative", + background: "#111827", + borderRadius: "0.75rem", + padding: "0.75rem", + maxWidth: "90vw", + maxHeight: "90vh", + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "0.5rem", + overflow: "visible", }} > {/* Previous page */} @@ -216,22 +216,22 @@ const PagePreviewModal: React.FC = ({ }} disabled={!canGoPrevious} style={{ - position: 'absolute', + position: "absolute", left: 0, - top: '50%', - transform: 'translate(-50%, -50%)', - width: '2.5rem', - height: '2.5rem', - borderRadius: '999px', - border: 'none', - background: canGoPrevious ? '#374151' : '#1f2937', - color: canGoPrevious ? '#e5e7eb' : '#6b7280', - cursor: canGoPrevious ? 'pointer' : 'default', - fontSize: '1.35rem', + top: "50%", + transform: "translate(-50%, -50%)", + width: "2.5rem", + height: "2.5rem", + borderRadius: "999px", + border: "none", + background: canGoPrevious ? "#374151" : "#1f2937", + color: canGoPrevious ? "#e5e7eb" : "#6b7280", + cursor: canGoPrevious ? "pointer" : "default", + fontSize: "1.35rem", lineHeight: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', + display: "flex", + alignItems: "center", + justifyContent: "center", zIndex: 2, }} title="Previous page (←)" @@ -249,22 +249,22 @@ const PagePreviewModal: React.FC = ({ }} disabled={!canGoNext} style={{ - position: 'absolute', + position: "absolute", right: 0, - top: '50%', - transform: 'translate(50%, -50%)', - width: '2.5rem', - height: '2.5rem', - borderRadius: '999px', - border: 'none', - background: canGoNext ? '#374151' : '#1f2937', - color: canGoNext ? '#e5e7eb' : '#6b7280', - cursor: canGoNext ? 'pointer' : 'default', - fontSize: '1.35rem', + top: "50%", + transform: "translate(50%, -50%)", + width: "2.5rem", + height: "2.5rem", + borderRadius: "999px", + border: "none", + background: canGoNext ? "#374151" : "#1f2937", + color: canGoNext ? "#e5e7eb" : "#6b7280", + cursor: canGoNext ? "pointer" : "default", + fontSize: "1.35rem", lineHeight: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', + display: "flex", + alignItems: "center", + justifyContent: "center", zIndex: 2, }} title="Next page (→)" @@ -281,22 +281,22 @@ const PagePreviewModal: React.FC = ({ onClose(); }} style={{ - position: 'absolute', + position: "absolute", top: 0, right: 0, - transform: 'translate(50%, -50%)', - width: '2.25rem', - height: '2.25rem', - borderRadius: '999px', - border: 'none', - background: '#374151', - color: '#e5e7eb', - cursor: 'pointer', - fontSize: '1.2rem', + transform: "translate(50%, -50%)", + width: "2.25rem", + height: "2.25rem", + borderRadius: "999px", + border: "none", + background: "#374151", + color: "#e5e7eb", + cursor: "pointer", + fontSize: "1.2rem", lineHeight: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', + display: "flex", + alignItems: "center", + justifyContent: "center", zIndex: 3, }} title="Close preview (Esc)" @@ -308,14 +308,14 @@ const PagePreviewModal: React.FC = ({ -
+
{positionLabel} · Original page {pageIndex + 1} · Rot {rotation}°
@@ -323,4 +323,4 @@ const PagePreviewModal: React.FC = ({ ); }; -export default PagePreviewModal; \ No newline at end of file +export default PagePreviewModal; diff --git a/src/components/PageWorkspace/CopyPagesDialog.tsx b/src/components/PageWorkspace/CopyPagesDialog.tsx new file mode 100644 index 0000000..6b4275a --- /dev/null +++ b/src/components/PageWorkspace/CopyPagesDialog.tsx @@ -0,0 +1,224 @@ +import React, { useEffect } from "react"; + +interface CopyPagesDialogProps { + selectedCount: number; + pageCount: number; + targetPosition: string; + error: string | null; + onTargetPositionChange: (value: string) => void; + onCancel: () => void; + onConfirm: (e?: React.FormEvent) => void; +} + +const CopyPagesDialog: React.FC = ({ + selectedCount, + pageCount, + targetPosition, + error, + onTargetPositionChange, + onCancel, + onConfirm, +}) => { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onCancel(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [onCancel]); + + return ( +
{ + if (e.target === e.currentTarget) { + onCancel(); + } + }} + style={{ + position: "fixed", + inset: 0, + zIndex: 60, + background: "rgba(15, 23, 42, 0.55)", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "1rem", + }} + > +
+
+

+ Copy selected pages +

+ + +
+ +

+ Copy{" "} + + {selectedCount === 1 + ? "1 selected page" + : `${selectedCount} selected pages`} + {" "} + to a new position. +

+ + + +
+
1 = before the first page
+
{pageCount + 1} = after the last page
+
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+
+ ); +}; + +export default CopyPagesDialog; diff --git a/src/components/PageWorkspace/DropIndicator.tsx b/src/components/PageWorkspace/DropIndicator.tsx new file mode 100644 index 0000000..2437960 --- /dev/null +++ b/src/components/PageWorkspace/DropIndicator.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +interface DropIndicatorProps { + side: "left" | "right" | "end"; + color: string; +} + +const DropIndicator: React.FC = ({ side, color }) => { + const isEnd = side === "end"; + + return ( +
+ ); +}; + +export default DropIndicator; diff --git a/src/components/PageWorkspace/PageCard.tsx b/src/components/PageWorkspace/PageCard.tsx new file mode 100644 index 0000000..7592616 --- /dev/null +++ b/src/components/PageWorkspace/PageCard.tsx @@ -0,0 +1,213 @@ +import React from "react"; +import type { PageRef } from "../../pdf/pdfTypes"; +import DropIndicator from "./DropIndicator"; + +interface PageCardProps { + page: PageRef; + visualIndex: number; + thumbnail?: string; + selected: boolean; + isDraggingCard: boolean; + isBusy: boolean; + isCopyDragging: boolean; + showLeftLine: boolean; + showRightLine: boolean; + dropIndicatorColor: string; + onDragStart: React.DragEventHandler; + onDragEnd: React.DragEventHandler; + onDragOver: React.DragEventHandler; + onOpenPreview: () => void; + onToggleSelect: React.MouseEventHandler; + onRotateClockwise: () => void; + onRotateCounterclockwise: () => void; + onDelete: () => void; +} + +const pageActionButtonStyle: React.CSSProperties = { + border: "none", + borderRadius: "999px", + padding: "0.15rem 0.4rem", + fontSize: "0.75rem", + cursor: "pointer", +}; + +const PageCard: React.FC = ({ + page, + visualIndex, + thumbnail, + selected, + isDraggingCard, + isBusy, + isCopyDragging, + showLeftLine, + showRightLine, + dropIndicatorColor, + onDragStart, + onDragEnd, + onDragOver, + onOpenPreview, + onToggleSelect, + onRotateClockwise, + onRotateCounterclockwise, + onDelete, +}) => { + const background = isDraggingCard + ? isCopyDragging + ? "#dcfce7" + : "#dbeafe" + : selected + ? "#eff6ff" + : "#f9fafb"; + + return ( +
+ + + {showLeftLine && } + {showRightLine && ( + + )} + +
+ {thumbnail ? ( + {`Page + ) : ( +
+ )} +
+ + + Page {page.sourcePageIndex + 1} + + + Pos {visualIndex + 1} · Rot {page.rotation}° + + +
+ + + + + +
+
+ ); +}; + +export default PageCard; diff --git a/src/components/PageWorkspace/PageGrid.tsx b/src/components/PageWorkspace/PageGrid.tsx new file mode 100644 index 0000000..2c23312 --- /dev/null +++ b/src/components/PageWorkspace/PageGrid.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import type { PageRef } from "../../pdf/pdfTypes"; +import DropIndicator from "./DropIndicator"; +import PageCard from "./PageCard"; + +interface PageGridProps { + pages: PageRef[]; + thumbnails: Record; + selectedPageIds: string[]; + isBusy: boolean; + draggingIndex: number | null; + dropIndex: number | null; + draggingSelectionActive: boolean; + isCopyDragging: boolean; + dropIndicatorColor: string; + onDragStart: (visualIndex: number) => React.DragEventHandler; + onDragEnd: React.DragEventHandler; + onCardDragOver: ( + visualIndex: number, + ) => React.DragEventHandler; + onEndSlotDragOver: React.DragEventHandler; + onDrop: React.DragEventHandler; + onOpenPreview: (pageId: string) => void; + onToggleSelect: ( + pageId: string, + visualIndex: number, + ) => React.MouseEventHandler; + onRotateClockwise: (pageId: string) => void; + onRotateCounterclockwise: (pageId: string) => void; + onDelete: (pageId: string) => void; +} + +const PageGrid: React.FC = ({ + pages, + thumbnails, + selectedPageIds, + isBusy, + draggingIndex, + dropIndex, + draggingSelectionActive, + isCopyDragging, + dropIndicatorColor, + onDragStart, + onDragEnd, + onCardDragOver, + onEndSlotDragOver, + onDrop, + onOpenPreview, + onToggleSelect, + onRotateClockwise, + onRotateCounterclockwise, + onDelete, +}) => { + const isSelected = (pageId: string) => selectedPageIds.includes(pageId); + + const showLeftLine = (visualIndex: number) => + dropIndex !== null && dropIndex === visualIndex && draggingIndex !== null; + + const showRightLine = (visualIndex: number) => + dropIndex !== null && + dropIndex === visualIndex + 1 && + draggingIndex !== null; + + const showEndLine = () => + dropIndex !== null && dropIndex === pages.length && draggingIndex !== null; + + return ( +
+ {pages.map((page, visualIndex) => { + const selected = isSelected(page.id); + const isDraggingCard = + draggingIndex != null && + ((draggingSelectionActive && selected) || + (!draggingSelectionActive && visualIndex === draggingIndex)); + + return ( + onOpenPreview(page.id)} + onToggleSelect={onToggleSelect(page.id, visualIndex)} + onRotateClockwise={() => onRotateClockwise(page.id)} + onRotateCounterclockwise={() => onRotateCounterclockwise(page.id)} + onDelete={() => onDelete(page.id)} + /> + ); + })} + + {pages.length > 0 && ( +
+ {showEndLine() && ( + + )} +
+ )} +
+ ); +}; + +export default PageGrid; diff --git a/src/components/PageWorkspace/PageSelectionToolbar.tsx b/src/components/PageWorkspace/PageSelectionToolbar.tsx new file mode 100644 index 0000000..68ec869 --- /dev/null +++ b/src/components/PageWorkspace/PageSelectionToolbar.tsx @@ -0,0 +1,112 @@ +import React from "react"; + +interface PageSelectionToolbarProps { + selectedCount: number; + onCopySelected: () => void; + onDeleteSelected: () => void; + onSelectAll: () => void; + onClearSelection: () => void; +} + +const pillButtonStyle: React.CSSProperties = { + border: "none", + borderRadius: "999px", + padding: "0.15rem 0.6rem", + fontSize: "0.8rem", +}; + +const PageSelectionToolbar: React.FC = ({ + selectedCount, + onCopySelected, + onDeleteSelected, + onSelectAll, + onClearSelection, +}) => { + const hasSelection = selectedCount > 0; + + return ( +
+ + Selected: {selectedCount} + + +
+ {hasSelection && ( + + )} + + {hasSelection && ( + + )} + + + + +
+
+ ); +}; + +export default PageSelectionToolbar; diff --git a/src/components/ReorderPanel.tsx b/src/components/ReorderPanel.tsx index d26e839..a109cc8 100644 --- a/src/components/ReorderPanel.tsx +++ b/src/components/ReorderPanel.tsx @@ -1,5 +1,8 @@ -import React, { useEffect, useState, useRef } from 'react'; -import type { PageRef } from '../pdf/pdfTypes'; +import React, { useRef, useState } from "react"; +import type { PageRef } from "../pdf/pdfTypes"; +import CopyPagesDialog from "./PageWorkspace/CopyPagesDialog"; +import PageGrid from "./PageWorkspace/PageGrid"; +import PageSelectionToolbar from "./PageWorkspace/PageSelectionToolbar"; interface ReorderPanelProps { pages: PageRef[]; @@ -17,7 +20,7 @@ interface ReorderPanelProps { onToggleSelect: ( pageId: string, visualIndex: number, - e: React.MouseEvent + e: React.MouseEvent, ) => void; onSelectAll: () => void; @@ -48,13 +51,11 @@ const ReorderPanel: React.FC = ({ const [isCopyDragging, setIsCopyDragging] = useState(false); const [copyDialogOpen, setCopyDialogOpen] = useState(false); - const [copyTargetPosition, setCopyTargetPosition] = useState(''); + const [copyTargetPosition, setCopyTargetPosition] = useState(""); const [copyDialogError, setCopyDialogError] = useState(null); const dragGhostRef = useRef(null); - const isSelected = (pageId: string) => selectedPageIds.includes(pageId); - const cleanupDragGhost = () => { if (dragGhostRef.current && dragGhostRef.current.parentNode) { dragGhostRef.current.parentNode.removeChild(dragGhostRef.current); @@ -71,7 +72,7 @@ const ReorderPanel: React.FC = ({ if (!draggedPage) return []; const selectedInVisualOrder = pages.filter((page) => - selectedPageIds.includes(page.id) + selectedPageIds.includes(page.id), ); const draggingIsSelected = @@ -84,20 +85,20 @@ const ReorderPanel: React.FC = ({ const createDragGhost = (e: React.DragEvent, count: number) => { cleanupDragGhost(); - const ghost = document.createElement('div'); - ghost.textContent = count === 1 ? '1 page' : `${count} pages`; + const ghost = document.createElement("div"); + ghost.textContent = count === 1 ? "1 page" : `${count} pages`; - ghost.style.position = 'fixed'; - ghost.style.top = '0'; - ghost.style.left = '0'; - ghost.style.padding = '4px 8px'; - ghost.style.borderRadius = '999px'; - ghost.style.background = '#111827'; - ghost.style.color = '#e5e7eb'; - ghost.style.fontSize = '12px'; + ghost.style.position = "fixed"; + ghost.style.top = "0"; + ghost.style.left = "0"; + ghost.style.padding = "4px 8px"; + ghost.style.borderRadius = "999px"; + ghost.style.background = "#111827"; + ghost.style.color = "#e5e7eb"; + ghost.style.fontSize = "12px"; ghost.style.fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; - ghost.style.zIndex = '9999'; + ghost.style.zIndex = "9999"; document.body.appendChild(ghost); dragGhostRef.current = ghost; @@ -106,6 +107,13 @@ const ReorderPanel: React.FC = ({ e.dataTransfer.setDragImage(ghost, rect.width / 2, rect.height / 2); }; + const resetDragState = () => { + cleanupDragGhost(); + setDraggingIndex(null); + setDropIndex(null); + setIsCopyDragging(false); + }; + const handleDragStart = (visualIndex: number) => (e: React.DragEvent) => { setDraggingIndex(visualIndex); setDropIndex(visualIndex); @@ -113,19 +121,16 @@ const ReorderPanel: React.FC = ({ const copying = isCopyModifierPressed(e); setIsCopyDragging(copying); - e.dataTransfer.effectAllowed = 'copyMove'; - e.dataTransfer.dropEffect = copying ? 'copy' : 'move'; - e.dataTransfer.setData('text/plain', String(visualIndex)); + e.dataTransfer.effectAllowed = "copyMove"; + e.dataTransfer.dropEffect = copying ? "copy" : "move"; + e.dataTransfer.setData("text/plain", String(visualIndex)); const draggedPages = getDraggedPages(visualIndex); createDragGhost(e, draggedPages.length); }; const handleDragEnd = () => { - cleanupDragGhost(); - setDraggingIndex(null); - setDropIndex(null); - setIsCopyDragging(false); + resetDragState(); }; const handleCardDragOver = (visualIndex: number) => (e: React.DragEvent) => { @@ -136,7 +141,7 @@ const ReorderPanel: React.FC = ({ const copying = isCopyModifierPressed(e); setIsCopyDragging(copying); - e.dataTransfer.dropEffect = copying ? 'copy' : 'move'; + e.dataTransfer.dropEffect = copying ? "copy" : "move"; const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect(); const x = e.clientX - rect.left; @@ -153,7 +158,7 @@ const ReorderPanel: React.FC = ({ const copying = isCopyModifierPressed(e); setIsCopyDragging(copying); - e.dataTransfer.dropEffect = copying ? 'copy' : 'move'; + e.dataTransfer.dropEffect = copying ? "copy" : "move"; setDropIndex(pages.length); }; @@ -172,7 +177,7 @@ const ReorderPanel: React.FC = ({ if (shouldCopy) { onCopyPagesToSlot( draggedPages.map((page) => page.id), - dropIndex + dropIndex, ); setDraggingIndex(null); @@ -207,35 +212,23 @@ const ReorderPanel: React.FC = ({ setIsCopyDragging(false); }; - const handleDeleteClick = (pageId: string) => () => { + const handleDeleteClick = (pageId: string) => { onDelete(pageId); setDraggingIndex(null); setDropIndex(null); }; - const handleRotateClickClockwise = (pageId: string) => () => { - onRotateClockwise(pageId); - }; - - const handleRotateClickCounterclockwise = (pageId: string) => () => { - onRotateCounterclockwise(pageId); - }; - - const handleCardClick = (pageId: string) => () => { - onOpenPreview(pageId); - }; - const handleCheckboxClick = (pageId: string, visualIndex: number) => - (e: React.MouseEvent) => { - e.stopPropagation(); // don't trigger preview - onToggleSelect(pageId, visualIndex, e); - }; + (e: React.MouseEvent) => { + e.stopPropagation(); + onToggleSelect(pageId, visualIndex, e); + }; const handleCopySelectedClick = () => { if (selectedPageIds.length === 0) return; - setCopyTargetPosition(String(pages.length + 1)); // default: after last page + setCopyTargetPosition(String(pages.length + 1)); setCopyDialogError(null); setCopyDialogOpen(true); }; @@ -245,11 +238,16 @@ const ReorderPanel: React.FC = ({ setCopyDialogError(null); }; + const handleCopyTargetPositionChange = (value: string) => { + setCopyTargetPosition(value); + setCopyDialogError(null); + }; + const handleCopyDialogConfirm = (e?: React.FormEvent) => { e?.preventDefault(); if (selectedPageIds.length === 0) { - setCopyDialogError('No pages selected.'); + setCopyDialogError("No pages selected."); return; } @@ -267,23 +265,6 @@ const ReorderPanel: React.FC = ({ setCopyDialogError(null); }; - useEffect(() => { - if (!copyDialogOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - handleCopyDialogCancel(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [copyDialogOpen]); - if (!hasPdf) { return (
@@ -293,554 +274,66 @@ const ReorderPanel: React.FC = ({ ); } - const showLeftLine = (visualIndex: number) => - dropIndex !== null && dropIndex === visualIndex && draggingIndex !== null; - - const showRightLine = (visualIndex: number) => - dropIndex !== null && - dropIndex === visualIndex + 1 && - draggingIndex !== null; - - const showEndLine = () => - dropIndex !== null && dropIndex === pages.length && draggingIndex !== null; - - // For highlighting the whole selection while dragging it const draggingPage = draggingIndex != null ? pages[draggingIndex] : null; const draggingSelectionActive = draggingPage != null && selectedPageIds.length > 0 && selectedPageIds.includes(draggingPage.id); - const dropIndicatorColor = isCopyDragging ? '#16a34a' : '#2563eb'; + const dropIndicatorColor = isCopyDragging ? "#16a34a" : "#2563eb"; return ( <>

Pages

-

+

Tap/click a page to preview it. Use the checkbox to select pages - (Shift for ranges). Drag to reorder; dragging a selected page moves the - whole selection. Hold Ctrl/⌘ while dropping to copy instead of move. - Shortcuts: Ctrl/⌘+A selects all, Delete removes selected pages, Esc clears - selection. + (Shift for ranges). Drag to reorder; dragging a selected page moves + the whole selection. Hold Ctrl/⌘ while dropping to copy instead of + move. Shortcuts: Ctrl/⌘+A selects all, Delete removes selected pages, + Esc clears selection.

-
- - Selected: {selectedPageIds.length} - -
- {selectedPageIds.length > 0 && ( - - )} - {selectedPageIds.length > 0 && ( - - )} - - -
-
+ -
- {pages.map((page, visualIndex) => { - const thumb = thumbnails[page.id]; - const rotation = page.rotation; - const selected = isSelected(page.id); - - const isDraggingCard = - draggingIndex != null && - ((draggingSelectionActive && selected) || - (!draggingSelectionActive && visualIndex === draggingIndex)); - - return ( -
- {/* selection checkbox */} - - - {/* left drop indicator */} - {showLeftLine(visualIndex) && ( -
- )} - - {/* right drop indicator */} - {showRightLine(visualIndex) && ( -
- )} - -
- {thumb ? ( - {`Page - ) : ( -
- )} -
- - Page {page.sourcePageIndex + 1} - - Pos {visualIndex + 1} · Rot {rotation}° - - -
- - - -
-
- ); - })} - - {/* end slot for dropping after the last card */} - {pages.length > 0 && ( -
- {showEndLine() && ( -
- )} -
- )} -
+ onOpenPreview={onOpenPreview} + onToggleSelect={handleCheckboxClick} + onRotateClockwise={onRotateClockwise} + onRotateCounterclockwise={onRotateCounterclockwise} + onDelete={handleDeleteClick} + />
{copyDialogOpen && ( -
{ - if (e.target === e.currentTarget) { - handleCopyDialogCancel(); - } - }} - style={{ - position: 'fixed', - inset: 0, - zIndex: 60, - background: 'rgba(15, 23, 42, 0.55)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: '1rem', - }} - > -
-
-

- Copy selected pages -

- - -
- -

- Copy{' '} - - {selectedPageIds.length === 1 - ? '1 selected page' - : `${selectedPageIds.length} selected pages`} - {' '} - to a new position. -

- - - -
-
1 = before the first page
-
{pages.length + 1} = after the last page
-
- - {copyDialogError && ( -
- {copyDialogError} -
- )} - -
- - - -
-
-
+ )} ); diff --git a/src/components/WorkspacePanel.tsx b/src/components/WorkspacePanel.tsx index 3759c23..bb1e2ae 100644 --- a/src/components/WorkspacePanel.tsx +++ b/src/components/WorkspacePanel.tsx @@ -1,327 +1,327 @@ -import React from 'react'; -import type { WorkspaceSummary } from '../workspace/workspaceTypes'; -import type { WorkspaceCommandRecord } from '../workspace/workspaceCommands'; +import React from "react"; +import type { WorkspaceSummary } from "../workspace/workspaceTypes"; +import type { WorkspaceCommandRecord } from "../workspace/workspaceCommands"; interface WorkspacePanelProps { - hasPdf: boolean; - isBusy: boolean; + hasPdf: boolean; + isBusy: boolean; - activeWorkspaceId: string | null; - workspaceName: string; - workspaceDirty: boolean; - workspaceMessage: string | null; + activeWorkspaceId: string | null; + workspaceName: string; + workspaceDirty: boolean; + workspaceMessage: string | null; - workspaces: WorkspaceSummary[]; - history: WorkspaceCommandRecord[]; - redoHistory: WorkspaceCommandRecord[]; + workspaces: WorkspaceSummary[]; + history: WorkspaceCommandRecord[]; + redoHistory: WorkspaceCommandRecord[]; - onWorkspaceNameChange: (value: string) => void; - onSaveWorkspace: () => void; - onLoadWorkspace: (workspaceId: string) => void; - onDeleteWorkspace: (workspaceId: string) => void; - onRefreshWorkspaces: () => void; - onResetWorkspace: () => void; - onUndo: () => void; - onRedo: () => void; + onWorkspaceNameChange: (value: string) => void; + onSaveWorkspace: () => void; + onLoadWorkspace: (workspaceId: string) => void; + onDeleteWorkspace: (workspaceId: string) => void; + onRefreshWorkspaces: () => void; + onResetWorkspace: () => void; + onUndo: () => void; + onRedo: () => void; } const WorkspacePanel: React.FC = ({ - hasPdf, - isBusy, - activeWorkspaceId, - workspaceName, - workspaceDirty, - workspaceMessage, - workspaces, - history, - redoHistory, - onWorkspaceNameChange, - onSaveWorkspace, - onLoadWorkspace, - onDeleteWorkspace, - onRefreshWorkspaces, - onResetWorkspace, - onUndo, - onRedo, + hasPdf, + isBusy, + activeWorkspaceId, + workspaceName, + workspaceDirty, + workspaceMessage, + workspaces, + history, + redoHistory, + onWorkspaceNameChange, + onSaveWorkspace, + onLoadWorkspace, + onDeleteWorkspace, + onRefreshWorkspaces, + onResetWorkspace, + onUndo, + onRedo, }) => { - const canUndo = history.length > 0; - const canRedo = redoHistory.length > 0; + const canUndo = history.length > 0; + const canRedo = redoHistory.length > 0; - const latestUndo = history[history.length - 1]; - const latestRedo = redoHistory[redoHistory.length - 1]; + const latestUndo = history[history.length - 1]; + const latestRedo = redoHistory[redoHistory.length - 1]; - return ( -
-

Workspace

+ return ( +
+

Workspace

-

- Save named workspaces in this browser. PDF binaries are stored in - IndexedDB; nothing is uploaded. -

+

+ Save named workspaces in this browser. PDF binaries are stored in + IndexedDB; nothing is uploaded. +

+ +
+ onWorkspaceNameChange(e.target.value)} + placeholder="Workspace name" + disabled={!hasPdf || isBusy} + style={{ + flex: "1 1 220px", + minWidth: 0, + padding: "0.45rem 0.55rem", + borderRadius: "0.5rem", + border: "1px solid #d1d5db", + fontSize: "0.9rem", + }} + /> + + + + + + + + + + +
+ + {workspaceDirty && hasPdf && ( +
+ Unsaved workspace changes. +
+ )} + + {workspaceMessage && ( +
+ {workspaceMessage} +
+ )} + + {workspaces.length > 0 && ( +
+ Saved workspaces + +
+ {workspaces.map((workspace) => { + const active = workspace.id === activeWorkspaceId; + + return ( +
+
+
+ {workspace.name} + {active && ( + · active + )} +
+ +
+ {workspace.pdfName} · source pages:{" "} + {workspace.sourcePageCount} · workspace pages:{" "} + {workspace.workspacePageCount} · undo:{" "} + {workspace.historyCount} · redo: {workspace.redoCount} · + updated {new Date(workspace.updatedAt).toLocaleString()} +
+
+ +
+ + + +
+
+ ); + })} +
+
+ )} + + {(history.length > 0 || redoHistory.length > 0) && ( +
+ + Command history ({history.length} undo / {redoHistory.length} redo) + + +
+ {history.map((entry, index) => ( +
+ + Undo {history.length - index}. {entry.label} + +
+ + {new Date(entry.timestamp).toLocaleString()} + +
+ ))}
- onWorkspaceNameChange(e.target.value)} - placeholder="Workspace name" - disabled={!hasPdf || isBusy} - style={{ - flex: '1 1 220px', - minWidth: 0, - padding: '0.45rem 0.55rem', - borderRadius: '0.5rem', - border: '1px solid #d1d5db', - fontSize: '0.9rem', - }} - /> + style={{ + margin: "0.25rem 0", + borderRadius: "999px", + background: "#ecfdf5", + color: "#166534", + fontSize: "0.8rem", + fontWeight: 600, + alignSelf: "flex-start", + border: "2px solid #166534", + width: "100%", + }} + >
- - - - - - - - - -
- - {workspaceDirty && hasPdf && ( + {redoHistory + .slice() + .reverse() + .map((entry, index) => (
- Unsaved workspace changes. + + Redo {index + 1}. {entry.label} + +
+ + {new Date(entry.timestamp).toLocaleString()} +
- )} - - {workspaceMessage && ( -
- {workspaceMessage} -
- )} - - {workspaces.length > 0 && ( -
- Saved workspaces - -
- {workspaces.map((workspace) => { - const active = workspace.id === activeWorkspaceId; - - return ( -
-
-
- {workspace.name} - {active && ( - · active - )} -
- -
- {workspace.pdfName} · source pages:{' '} - {workspace.sourcePageCount} · workspace pages:{' '} - {workspace.workspacePageCount} · undo:{' '} - {workspace.historyCount} · redo: {workspace.redoCount} · updated{' '} - {new Date(workspace.updatedAt).toLocaleString()} -
-
- -
- - - -
-
- ); - })} -
-
- )} - - {(history.length > 0 || redoHistory.length > 0) && ( -
- - Command history ({history.length} undo / {redoHistory.length} redo) - - -
- {history.map((entry, index) => ( -
- - Undo {history.length - index}. {entry.label} - -
- - {new Date(entry.timestamp).toLocaleString()} - -
- ))} - -
- -
- - {redoHistory - .slice() - .reverse() - .map((entry, index) => ( -
- - Redo {index + 1}. {entry.label} - -
- - {new Date(entry.timestamp).toLocaleString()} - -
- ))} -
-
- )} -
- ); + ))} +
+ + )} +
+ ); }; -export default WorkspacePanel; \ No newline at end of file +export default WorkspacePanel; diff --git a/src/hooks/usePdfGeneratedOutputs.ts b/src/hooks/usePdfGeneratedOutputs.ts new file mode 100644 index 0000000..20bb370 --- /dev/null +++ b/src/hooks/usePdfGeneratedOutputs.ts @@ -0,0 +1,128 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import type { SplitResult } from "../pdf/pdfTypes"; + +export interface PdfDownload { + id: string; + filename: string; + url: string; +} + +export interface SplitPdfDownload extends PdfDownload { + pageIndex: number; +} + +function revokeDownload(download: PdfDownload | null): void { + if (download) { + URL.revokeObjectURL(download.url); + } +} + +function revokeDownloads(downloads: PdfDownload[]): void { + downloads.forEach(revokeDownload); +} + +function createDownload(id: string, filename: string, blob: Blob): PdfDownload { + return { + id, + filename, + url: URL.createObjectURL(blob), + }; +} + +export function usePdfGeneratedOutputs() { + const [splitDownloads, setSplitDownloads] = useState([]); + const [subsetDownload, setSubsetDownload] = useState( + null, + ); + const [exportDownload, setExportDownload] = useState( + null, + ); + + const splitDownloadsRef = useRef([]); + const subsetDownloadRef = useRef(null); + const exportDownloadRef = useRef(null); + + const replaceSplitResults = useCallback((results: SplitResult[]) => { + const nextDownloads: SplitPdfDownload[] = results.map((result) => ({ + ...createDownload( + `split-${result.pageIndex}-${result.filename}`, + result.filename, + result.blob, + ), + pageIndex: result.pageIndex, + })); + + revokeDownloads(splitDownloadsRef.current); + splitDownloadsRef.current = nextDownloads; + setSplitDownloads(nextDownloads); + }, []); + + const clearSplitResults = useCallback(() => { + revokeDownloads(splitDownloadsRef.current); + splitDownloadsRef.current = []; + setSplitDownloads([]); + }, []); + + const replaceSubsetResult = useCallback((blob: Blob, filename: string) => { + const nextDownload = createDownload("subset", filename, blob); + + revokeDownload(subsetDownloadRef.current); + subsetDownloadRef.current = nextDownload; + setSubsetDownload(nextDownload); + }, []); + + const clearSubsetResult = useCallback(() => { + revokeDownload(subsetDownloadRef.current); + subsetDownloadRef.current = null; + setSubsetDownload(null); + }, []); + + const replaceExportResult = useCallback((blob: Blob, filename: string) => { + const nextDownload = createDownload("export", filename, blob); + + revokeDownload(exportDownloadRef.current); + exportDownloadRef.current = nextDownload; + setExportDownload(nextDownload); + }, []); + + const clearExportResult = useCallback(() => { + revokeDownload(exportDownloadRef.current); + exportDownloadRef.current = null; + setExportDownload(null); + }, []); + + const clearAllResults = useCallback(() => { + revokeDownloads(splitDownloadsRef.current); + revokeDownload(subsetDownloadRef.current); + revokeDownload(exportDownloadRef.current); + + splitDownloadsRef.current = []; + subsetDownloadRef.current = null; + exportDownloadRef.current = null; + + setSplitDownloads([]); + setSubsetDownload(null); + setExportDownload(null); + }, []); + + useEffect(() => { + return () => { + revokeDownloads(splitDownloadsRef.current); + revokeDownload(subsetDownloadRef.current); + revokeDownload(exportDownloadRef.current); + }; + }, []); + + return { + splitDownloads, + subsetDownload, + exportDownload, + replaceSplitResults, + clearSplitResults, + replaceSubsetResult, + clearSubsetResult, + replaceExportResult, + clearExportResult, + clearAllResults, + }; +} diff --git a/src/main.tsx b/src/main.tsx index 5610739..81da346 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import './styles.css'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles.css"; -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + , ); diff --git a/src/pdf/pdfService.ts b/src/pdf/pdfService.ts index 3891406..2bb1fd2 100644 --- a/src/pdf/pdfService.ts +++ b/src/pdf/pdfService.ts @@ -1,10 +1,20 @@ -import { PDFDocument, degrees } from 'pdf-lib'; -import type { PdfFile, PageRef, SplitResult, Range } from './pdfTypes'; +import { PDFDocument, degrees } from "pdf-lib"; +import type { PdfFile, PageRef, SplitResult, Range } from "./pdfTypes"; function createId() { return Math.random().toString(36).slice(2); } +function pdfBytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer { + const buffer = new ArrayBuffer(bytes.byteLength); + new Uint8Array(buffer).set(bytes); + return buffer; +} + +function pdfBytesToBlob(bytes: Uint8Array): Blob { + return new Blob([pdfBytesToArrayBuffer(bytes)], { type: "application/pdf" }); +} + export async function loadPdfFromFile(file: File): Promise { const arrayBuffer = await file.arrayBuffer(); const doc = await PDFDocument.load(arrayBuffer); @@ -21,10 +31,10 @@ export async function loadPdfFromFile(file: File): Promise { export async function mergePdfFiles( basePdf: PdfFile, newPdf: PdfFile, - insertAt: number + insertAt: number, ): Promise { - const baseDoc = basePdf.doc ?? await PDFDocument.load(basePdf.arrayBuffer); - const newDoc = newPdf.doc ?? await PDFDocument.load(newPdf.arrayBuffer); + const baseDoc = basePdf.doc ?? (await PDFDocument.load(basePdf.arrayBuffer)); + const newDoc = newPdf.doc ?? (await PDFDocument.load(newPdf.arrayBuffer)); const mergedDoc = await PDFDocument.create(); @@ -35,11 +45,11 @@ export async function mergePdfFiles( const basePages = await mergedDoc.copyPages( baseDoc, - Array.from({ length: basePageCount }, (_, i) => i) + Array.from({ length: basePageCount }, (_, i) => i), ); const newPages = await mergedDoc.copyPages( newDoc, - Array.from({ length: newPageCount }, (_, i) => i) + Array.from({ length: newPageCount }, (_, i) => i), ); for (let i = 0; i < clampedInsertAt; i += 1) { @@ -53,11 +63,10 @@ export async function mergePdfFiles( } const bytes = await mergedDoc.save(); - const buffer = new ArrayBuffer(bytes.byteLength); - new Uint8Array(buffer).set(bytes); + const buffer = pdfBytesToArrayBuffer(bytes); - const baseName = basePdf.name.replace(/\.pdf$/i, ''); - const newName = newPdf.name.replace(/\.pdf$/i, ''); + const baseName = basePdf.name.replace(/\.pdf$/i, ""); + const newName = newPdf.name.replace(/\.pdf$/i, ""); return { id: createId(), @@ -69,7 +78,7 @@ export async function mergePdfFiles( } export async function splitIntoSinglePages( - pdf: PdfFile + pdf: PdfFile, ): Promise { const { doc, name } = pdf; @@ -99,10 +108,10 @@ export async function splitIntoSinglePages( if (modificationDate) newDoc.setModificationDate(modificationDate); const bytes = await newDoc.save(); - const blob = new Blob([bytes], { type: 'application/pdf' }); + const blob = pdfBytesToBlob(bytes); - const base = name.replace(/\.pdf$/i, ''); - const filename = `${base}_page_${String(i + 1).padStart(3, '0')}.pdf`; + const base = name.replace(/\.pdf$/i, ""); + const filename = `${base}_page_${String(i + 1).padStart(3, "0")}.pdf`; results.push({ pageIndex: i, @@ -114,10 +123,7 @@ export async function splitIntoSinglePages( return results; } -export async function extractRange( - pdf: PdfFile, - range: Range -): Promise { +export async function extractRange(pdf: PdfFile, range: Range): Promise { const { doc } = pdf; const pageCount = doc.getPageCount(); @@ -125,7 +131,7 @@ export async function extractRange( const toIndex = Math.min(pageCount - 1, range.to - 1); if (fromIndex > toIndex) { - throw new Error('Invalid range: from > to'); + throw new Error("Invalid range: from > to"); } const newDoc = await PDFDocument.create(); @@ -136,7 +142,7 @@ export async function extractRange( copiedPages.forEach((p) => newDoc.addPage(p)); const bytes = await newDoc.save(); - return new Blob([bytes], { type: 'application/pdf' }); + return pdfBytesToBlob(bytes); } export async function mergePdfs(pdfs: PdfFile[]): Promise { @@ -150,26 +156,26 @@ export async function mergePdfs(pdfs: PdfFile[]): Promise { } const bytes = await newDoc.save(); - return new Blob([bytes], { type: 'application/pdf' }); + return pdfBytesToBlob(bytes); } export async function exportPages( pdf: PdfFile, - pages: PageRef[] + pages: PageRef[], ): Promise { const { doc } = pdf; const pageCount = doc.getPageCount(); if (pages.length === 0) { - throw new Error('Pages must contain at least one page'); + throw new Error("Pages must contain at least one page"); } if ( pages.some( - (page) => page.sourcePageIndex < 0 || page.sourcePageIndex >= pageCount + (page) => page.sourcePageIndex < 0 || page.sourcePageIndex >= pageCount, ) ) { - throw new Error('Pages contain invalid source page indices'); + throw new Error("Pages contain invalid source page indices"); } const newDoc = await PDFDocument.create(); @@ -180,7 +186,7 @@ export async function exportPages( copiedPages.forEach((page, idx) => { const angle = pages[idx].rotation; - if (typeof angle === 'number' && angle % 360 !== 0) { + if (typeof angle === "number" && angle % 360 !== 0) { page.setRotation(degrees(angle)); } @@ -188,13 +194,13 @@ export async function exportPages( }); const bytes = await newDoc.save(); - return new Blob([bytes], { type: 'application/pdf' }); + return pdfBytesToBlob(bytes); } export async function exportReordered( pdf: PdfFile, order: number[], - rotations?: Record + rotations?: Record, ): Promise { return exportPages( pdf, @@ -202,6 +208,6 @@ export async function exportReordered( id: String(sourcePageIndex), sourcePageIndex, rotation: rotations?.[sourcePageIndex] ?? 0, - })) + })), ); -} \ No newline at end of file +} diff --git a/src/pdf/pdfThumbnailService.ts b/src/pdf/pdfThumbnailService.ts index 4bfdff1..292afe0 100644 --- a/src/pdf/pdfThumbnailService.ts +++ b/src/pdf/pdfThumbnailService.ts @@ -1,5 +1,5 @@ -import * as pdfjsLib from 'pdfjs-dist'; -import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url'; +import * as pdfjsLib from "pdfjs-dist"; +import pdfjsWorker from "pdfjs-dist/build/pdf.worker?worker&url"; // pdf.js worker setup for Vite // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -37,13 +37,12 @@ interface ThumbnailGenerationOptions { onThumbnail?: (update: ThumbnailUpdate) => void; } - /** * Unrotated thumbnails – used e.g. in the Split/Extract view. */ export async function generateThumbnailsProgressive( arrayBuffer: ArrayBuffer, - options: ThumbnailGenerationOptions = {} + options: ThumbnailGenerationOptions = {}, ): Promise { return generateThumbnailsInternal(arrayBuffer, {}, options); } @@ -54,7 +53,7 @@ export async function generateThumbnailsProgressive( export async function generateThumbnailsWithRotationsProgressive( arrayBuffer: ArrayBuffer, rotations: RotationsMap, - options: ThumbnailGenerationOptions = {} + options: ThumbnailGenerationOptions = {}, ): Promise { return generateThumbnailsInternal(arrayBuffer, rotations, options); } @@ -62,7 +61,7 @@ export async function generateThumbnailsWithRotationsProgressive( async function generateThumbnailsInternal( arrayBuffer: ArrayBuffer, rotations: RotationsMap, - options: ThumbnailGenerationOptions = {} + options: ThumbnailGenerationOptions = {}, ): Promise { const maxHeight = options.maxHeight ?? 150; const maxWidth = options.maxWidth ?? 140; @@ -73,15 +72,15 @@ async function generateThumbnailsInternal( const loadingTask = pdfjsLib.getDocument({ data: dataCopy }); const pdf = await loadingTask.promise; - const thumbs = Array(pdf.numPages).fill(''); + const thumbs = Array(pdf.numPages).fill(""); const pageNums = options.pageIndices ? Array.from( new Set( options.pageIndices .filter((pageIndex) => pageIndex >= 0 && pageIndex < pdf.numPages) - .map((pageIndex) => pageIndex + 1) - ) + .map((pageIndex) => pageIndex + 1), + ), ) : Array.from({ length: pdf.numPages }, (_, index) => index + 1); @@ -99,7 +98,7 @@ async function generateThumbnailsInternal( pageIndex, rotations, maxHeight, - maxWidth + maxWidth, ); if (signal?.aborted) return; @@ -112,7 +111,7 @@ async function generateThumbnailsInternal( while (!signal?.aborted) { const pageNum = pageNums[nextPageIndex]; nextPageIndex += 1; - + if (pageNum == null) return; await renderOne(pageNum); @@ -133,11 +132,15 @@ async function generateThumbnailsInternal( } async function renderPageThumbnail( - page: Awaited['promise']>['getPage']>>, + page: Awaited< + ReturnType< + Awaited["promise"]>["getPage"] + > + >, originalIndex: number, rotations: RotationsMap, maxHeight: number, - maxWidth: number + maxWidth: number, ): Promise { const viewport = page.getViewport({ scale: 1 }); const scaleH = maxHeight / viewport.height; @@ -145,10 +148,10 @@ async function renderPageThumbnail( const scale = Math.min(scaleH, scaleW); const scaledViewport = page.getViewport({ scale }); - const baseCanvas = document.createElement('canvas'); - const baseCtx = baseCanvas.getContext('2d'); + const baseCanvas = document.createElement("canvas"); + const baseCtx = baseCanvas.getContext("2d"); - if (!baseCtx) return ''; + if (!baseCtx) return ""; baseCanvas.width = scaledViewport.width; baseCanvas.height = scaledViewport.height; @@ -164,14 +167,14 @@ async function renderPageThumbnail( const rotationDeg = ((rotationDegRaw % 360) + 360) % 360; if (rotationDeg === 0) { - return baseCanvas.toDataURL('image/png'); + return baseCanvas.toDataURL("image/png"); } - const rotatedCanvas = document.createElement('canvas'); - const rotatedCtx = rotatedCanvas.getContext('2d'); + const rotatedCanvas = document.createElement("canvas"); + const rotatedCtx = rotatedCanvas.getContext("2d"); if (!rotatedCtx) { - return baseCanvas.toDataURL('image/png'); + return baseCanvas.toDataURL("image/png"); } const rad = (rotationDeg * Math.PI) / 180; @@ -204,5 +207,5 @@ async function renderPageThumbnail( rotatedCtx.drawImage(baseCanvas, 0, 0); rotatedCtx.restore(); - return rotatedCanvas.toDataURL('image/png'); -} \ No newline at end of file + return rotatedCanvas.toDataURL("image/png"); +} diff --git a/src/pdf/pdfTypes.ts b/src/pdf/pdfTypes.ts index e84e997..81a02e7 100644 --- a/src/pdf/pdfTypes.ts +++ b/src/pdf/pdfTypes.ts @@ -1,4 +1,4 @@ -import type { PDFDocument } from 'pdf-lib'; +import type { PDFDocument } from "pdf-lib"; export interface PdfFile { id: string; diff --git a/src/pdf/usePdfThumbnails.ts b/src/pdf/usePdfThumbnails.ts new file mode 100644 index 0000000..0a5eaea --- /dev/null +++ b/src/pdf/usePdfThumbnails.ts @@ -0,0 +1,212 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import type { PageRef, PdfFile } from "./pdfTypes"; +import { generateThumbnailsWithRotationsProgressive } from "./pdfThumbnailService"; +import { normalizeRotation } from "../workspace/useWorkspaceState"; + +const DEFAULT_MAX_HEIGHT = 150; +const DEFAULT_MAX_WIDTH = 140; +const DEFAULT_CONCURRENCY = 3; + +interface UsePdfThumbnailsOptions { + pdf: PdfFile | null; + pages: PageRef[]; + maxHeight?: number; + maxWidth?: number; + concurrency?: number; + onError?: (message: string, error: unknown) => void; +} + +interface UsePdfThumbnailsResult { + thumbnails: Record; + resetThumbnails: () => void; + clearThumbnailCache: () => void; +} + +function thumbnailCacheKey( + pdfId: string, + sourcePageIndex: number, + rotation: number, + maxWidth: number, + maxHeight: number, +): string { + return [ + pdfId, + sourcePageIndex, + normalizeRotation(rotation), + maxWidth, + maxHeight, + ].join(":"); +} + +function pruneAndMergeThumbnails( + previous: Record, + pages: PageRef[], + updates: Record, +): Record { + const pageIds = new Set(pages.map((page) => page.id)); + const next: Record = {}; + + for (const [pageId, thumbnail] of Object.entries(previous)) { + if (pageIds.has(pageId)) { + next[pageId] = thumbnail; + } + } + + return { + ...next, + ...updates, + }; +} + +export function usePdfThumbnails({ + pdf, + pages, + maxHeight = DEFAULT_MAX_HEIGHT, + maxWidth = DEFAULT_MAX_WIDTH, + concurrency = DEFAULT_CONCURRENCY, + onError, +}: UsePdfThumbnailsOptions): UsePdfThumbnailsResult { + const [thumbnails, setThumbnails] = useState>({}); + const thumbnailCacheRef = useRef>(new Map()); + const latestPagesRef = useRef(pages); + const previousPdfIdRef = useRef(null); + + useEffect(() => { + latestPagesRef.current = pages; + }, [pages]); + + const resetThumbnails = useCallback(() => { + setThumbnails({}); + }, []); + + const clearThumbnailCache = useCallback(() => { + thumbnailCacheRef.current.clear(); + setThumbnails({}); + }, []); + + useEffect(() => { + const currentPdfId = pdf?.id ?? null; + + if (!pdf) { + previousPdfIdRef.current = null; + clearThumbnailCache(); + return; + } + + if (previousPdfIdRef.current !== currentPdfId) { + previousPdfIdRef.current = currentPdfId; + clearThumbnailCache(); + } + }, [clearThumbnailCache, pdf]); + + useEffect(() => { + if (!pdf) return; + + const controller = new AbortController(); + const cachedUpdates: Record = {}; + const renderGroups = new Map>(); + + for (const page of pages) { + const rotation = normalizeRotation(page.rotation); + const cacheKey = thumbnailCacheKey( + pdf.id, + page.sourcePageIndex, + rotation, + maxWidth, + maxHeight, + ); + const cached = thumbnailCacheRef.current.get(cacheKey); + + if (cached) { + cachedUpdates[page.id] = cached; + continue; + } + + const pageIndices = renderGroups.get(rotation) ?? new Set(); + pageIndices.add(page.sourcePageIndex); + renderGroups.set(rotation, pageIndices); + } + + setThumbnails((previous) => + pruneAndMergeThumbnails(previous, pages, cachedUpdates), + ); + + if (renderGroups.size === 0) return; + + const renderMissingThumbnails = async () => { + for (const [rotation, pageIndexSet] of renderGroups) { + if (controller.signal.aborted) return; + + const pageIndices = Array.from(pageIndexSet); + const rotationsBySourcePage: Record = {}; + + for (const pageIndex of pageIndices) { + rotationsBySourcePage[pageIndex] = rotation; + } + + await generateThumbnailsWithRotationsProgressive( + pdf.arrayBuffer, + rotationsBySourcePage, + { + maxHeight, + maxWidth, + concurrency: Math.min(concurrency, pageIndices.length), + pageIndices, + signal: controller.signal, + onThumbnail: ({ pageIndex, dataUrl }) => { + if (controller.signal.aborted) return; + + thumbnailCacheRef.current.set( + thumbnailCacheKey( + pdf.id, + pageIndex, + rotation, + maxWidth, + maxHeight, + ), + dataUrl, + ); + + const updates: Record = {}; + + for (const page of latestPagesRef.current) { + if ( + page.sourcePageIndex === pageIndex && + normalizeRotation(page.rotation) === rotation + ) { + updates[page.id] = dataUrl; + } + } + + if (Object.keys(updates).length === 0) return; + + setThumbnails((previous) => + pruneAndMergeThumbnails( + previous, + latestPagesRef.current, + updates, + ), + ); + }, + }, + ); + } + }; + + void renderMissingThumbnails().catch((error) => { + if (!controller.signal.aborted) { + onError?.("Failed to generate thumbnails (see console).", error); + } + }); + + return () => { + controller.abort(); + }; + }, [concurrency, maxHeight, maxWidth, onError, pages, pdf]); + + return { + thumbnails, + resetThumbnails, + clearThumbnailCache, + }; +} diff --git a/src/styles.css b/src/styles.css index 2aabf50..3fa6d6f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -6,7 +6,11 @@ body { margin: 0; - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", sans-serif; background-color: #f3f4f6; color: #111827; diff --git a/src/version.ts b/src/version.ts index 80b5224..e59bb90 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const APP_VERSION = '0.2.0'; \ No newline at end of file +export const APP_VERSION = "0.2.1"; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/workspace/useWorkspaceState.test.tsx b/src/workspace/useWorkspaceState.test.tsx new file mode 100644 index 0000000..4905213 --- /dev/null +++ b/src/workspace/useWorkspaceState.test.tsx @@ -0,0 +1,208 @@ +import React, { forwardRef, useImperativeHandle } from "react"; +import { act, render } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { PageRef } from "../pdf/pdfTypes"; +import type { + WorkspaceCommandRecord, + WorkspaceCommandState, +} from "./workspaceCommands"; +import { useWorkspaceState } from "./useWorkspaceState"; + +function page(id: string, sourcePageIndex: number, rotation = 0): PageRef { + return { id, sourcePageIndex, rotation }; +} + +function state( + pages: PageRef[], + selectedPageIds: string[] = [], + lastSelectedVisualIndex: number | null = null, +): WorkspaceCommandState { + return { pages, selectedPageIds, lastSelectedVisualIndex }; +} + +interface HarnessRef { + snapshot: () => { + pages: PageRef[]; + selectedPageIds: string[]; + lastSelectedVisualIndex: number | null; + workspaceDirty: boolean; + workspaceMessage: string | null; + workspaceHistory: WorkspaceCommandRecord[]; + redoHistory: WorkspaceCommandRecord[]; + }; + replaceWorkspaceState: ReturnType< + typeof useWorkspaceState + >["replaceWorkspaceState"]; + getCurrentCommandState: ReturnType< + typeof useWorkspaceState + >["getCurrentCommandState"]; + createWorkspaceCommand: ReturnType< + typeof useWorkspaceState + >["createWorkspaceCommand"]; + executeWorkspaceCommand: ReturnType< + typeof useWorkspaceState + >["executeWorkspaceCommand"]; + handleUndo: ReturnType["handleUndo"]; + handleRedo: ReturnType["handleRedo"]; +} + +const Harness = forwardRef void }>( + ({ onContentChanged }, ref) => { + const workspace = useWorkspaceState({ onContentChanged }); + + useImperativeHandle(ref, () => ({ + snapshot: () => ({ + pages: workspace.pages, + selectedPageIds: workspace.selectedPageIds, + lastSelectedVisualIndex: workspace.lastSelectedVisualIndex, + workspaceDirty: workspace.workspaceDirty, + workspaceMessage: workspace.workspaceMessage, + workspaceHistory: workspace.workspaceHistory, + redoHistory: workspace.redoHistory, + }), + replaceWorkspaceState: workspace.replaceWorkspaceState, + getCurrentCommandState: workspace.getCurrentCommandState, + createWorkspaceCommand: workspace.createWorkspaceCommand, + executeWorkspaceCommand: workspace.executeWorkspaceCommand, + handleUndo: workspace.handleUndo, + handleRedo: workspace.handleRedo, + })); + + return null; + }, +); + +function renderHarness(onContentChanged = vi.fn()) { + const ref = React.createRef(); + render(); + + if (!ref.current) { + throw new Error("Harness ref was not initialized"); + } + + return { ref, onContentChanged }; +} + +describe("useWorkspaceState", () => { + it("replaces workspace state from loaded data without marking it dirty", () => { + const { ref } = renderHarness(); + const loadedPages = [page("p1", 0), page("p2", 1, 90)]; + + act(() => { + ref.current?.replaceWorkspaceState({ + pages: loadedPages, + selectedPageIds: ["p2"], + lastSelectedVisualIndex: 1, + history: [], + redoHistory: [], + dirty: false, + message: "Workspace loaded.", + }); + }); + + expect(ref.current?.snapshot()).toMatchObject({ + pages: loadedPages, + selectedPageIds: ["p2"], + lastSelectedVisualIndex: 1, + workspaceDirty: false, + workspaceMessage: "Workspace loaded.", + workspaceHistory: [], + redoHistory: [], + }); + }); + + it("executes commands, stores history, clears redo, and marks content changed", () => { + const { ref, onContentChanged } = renderHarness(); + const before = state([page("p1", 0), page("p2", 1)], ["p1"], 0); + const after = state([page("p2", 1), page("p1", 0)], ["p2"], 0); + + act(() => { + ref.current?.replaceWorkspaceState({ + ...before, + redoHistory: [ + { + id: "redo-record", + type: "old-redo", + label: "Old redo", + timestamp: "2026-05-17T10:00:00.000Z", + payload: { before, after }, + }, + ], + }); + }); + + act(() => { + const command = ref.current?.createWorkspaceCommand({ + type: "reorder-pages", + label: "Move page 2 before page 1", + before, + after, + }); + + if (!command) throw new Error("Command was not created"); + ref.current?.executeWorkspaceCommand(command); + }); + + const snapshot = ref.current?.snapshot(); + expect(snapshot?.pages).toEqual(after.pages); + expect(snapshot?.selectedPageIds).toEqual(["p2"]); + expect(snapshot?.workspaceDirty).toBe(true); + expect(snapshot?.workspaceMessage).toBeNull(); + expect(snapshot?.workspaceHistory).toHaveLength(1); + expect(snapshot?.workspaceHistory[0]).toMatchObject({ + type: "reorder-pages", + label: "Move page 2 before page 1", + }); + expect(snapshot?.redoHistory).toHaveLength(0); + expect(onContentChanged).toHaveBeenCalledTimes(1); + }); + + it("undoes and redoes command records in stack order", () => { + const { ref, onContentChanged } = renderHarness(); + const initial = state([page("p1", 0), page("p2", 1)], ["p1"], 0); + const reordered = state([page("p2", 1), page("p1", 0)], ["p2"], 0); + + act(() => { + ref.current?.replaceWorkspaceState(initial); + }); + + act(() => { + const command = ref.current?.createWorkspaceCommand({ + type: "reorder-pages", + label: "Move page", + before: initial, + after: reordered, + }); + + if (!command) throw new Error("Command was not created"); + ref.current?.executeWorkspaceCommand(command); + }); + + act(() => { + ref.current?.handleUndo(); + }); + + expect(ref.current?.snapshot()).toMatchObject({ + pages: initial.pages, + selectedPageIds: initial.selectedPageIds, + lastSelectedVisualIndex: initial.lastSelectedVisualIndex, + workspaceDirty: true, + }); + expect(ref.current?.snapshot().workspaceHistory).toHaveLength(0); + expect(ref.current?.snapshot().redoHistory).toHaveLength(1); + + act(() => { + ref.current?.handleRedo(); + }); + + expect(ref.current?.snapshot()).toMatchObject({ + pages: reordered.pages, + selectedPageIds: reordered.selectedPageIds, + lastSelectedVisualIndex: reordered.lastSelectedVisualIndex, + workspaceDirty: true, + }); + expect(ref.current?.snapshot().workspaceHistory).toHaveLength(1); + expect(ref.current?.snapshot().redoHistory).toHaveLength(0); + expect(onContentChanged).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/workspace/useWorkspaceState.ts b/src/workspace/useWorkspaceState.ts new file mode 100644 index 0000000..fd70759 --- /dev/null +++ b/src/workspace/useWorkspaceState.ts @@ -0,0 +1,246 @@ +import { useCallback, useRef, useState } from "react"; +import type { PageRef } from "../pdf/pdfTypes"; +import type { + WorkspaceCommand, + WorkspaceCommandRecord, + WorkspaceCommandState, +} from "./workspaceCommands"; +import { + createSnapshotCommand, + reviveWorkspaceCommand, + toWorkspaceCommandRecord, +} from "./workspaceCommands"; + +function createId(prefix: string): string { + if (typeof crypto !== "undefined" && crypto.randomUUID) { + return crypto.randomUUID(); + } + + return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`; +} + +export function createWorkspaceId(): string { + return createId("workspace"); +} + +export function defaultWorkspaceNameFromPdfName(pdfName: string): string { + return pdfName.replace(/\.pdf$/i, "") || "Untitled workspace"; +} + +export function createPageRefId(): string { + return createId("page"); +} + +export function createInitialPageRefs(pageCount: number): PageRef[] { + return Array.from({ length: pageCount }, (_, sourcePageIndex) => ({ + id: createPageRefId(), + sourcePageIndex, + rotation: 0, + })); +} + +export function normalizeRotation(rotation: number | undefined): number { + return (((rotation ?? 0) % 360) + 360) % 360; +} + +type SetStateAction = T | ((previous: T) => T); + +interface UseWorkspaceStateOptions { + onContentChanged?: () => void; +} + +interface ReplaceWorkspaceStateOptions { + pages?: PageRef[]; + selectedPageIds?: string[]; + lastSelectedVisualIndex?: number | null; + history?: WorkspaceCommandRecord[]; + redoHistory?: WorkspaceCommandRecord[]; + dirty?: boolean; + message?: string | null; +} + +export function useWorkspaceState({ + onContentChanged, +}: UseWorkspaceStateOptions = {}) { + const [pages, setPagesState] = useState([]); + const [selectedPageIds, setSelectedPageIdsState] = useState([]); + const [lastSelectedVisualIndex, setLastSelectedVisualIndexState] = useState< + number | null + >(null); + const [workspaceDirty, setWorkspaceDirty] = useState(false); + const [workspaceMessage, setWorkspaceMessage] = useState(null); + const [workspaceHistory, setWorkspaceHistory] = useState< + WorkspaceCommandRecord[] + >([]); + const [redoHistory, setRedoHistory] = useState([]); + + const latestPagesRef = useRef([]); + const selectedPageIdsRef = useRef([]); + const lastSelectedVisualIndexRef = useRef(null); + + const setPages = useCallback((action: SetStateAction) => { + setPagesState((previous) => { + const next = typeof action === "function" ? action(previous) : action; + latestPagesRef.current = next; + return next; + }); + }, []); + + const setSelectedPageIds = useCallback((action: SetStateAction) => { + setSelectedPageIdsState((previous) => { + const next = typeof action === "function" ? action(previous) : action; + selectedPageIdsRef.current = next; + return next; + }); + }, []); + + const setLastSelectedVisualIndex = useCallback( + (action: SetStateAction) => { + setLastSelectedVisualIndexState((previous) => { + const next = typeof action === "function" ? action(previous) : action; + lastSelectedVisualIndexRef.current = next; + return next; + }); + }, + [], + ); + + const getCurrentCommandState = useCallback( + (): WorkspaceCommandState => ({ + pages: latestPagesRef.current, + selectedPageIds: selectedPageIdsRef.current, + lastSelectedVisualIndex: lastSelectedVisualIndexRef.current, + }), + [], + ); + + const applyCommandState = useCallback( + (state: WorkspaceCommandState) => { + setPages(state.pages); + setSelectedPageIds(state.selectedPageIds); + setLastSelectedVisualIndex(state.lastSelectedVisualIndex); + }, + [setLastSelectedVisualIndex, setPages, setSelectedPageIds], + ); + + const markWorkspaceChanged = useCallback(() => { + setWorkspaceDirty(true); + setWorkspaceMessage(null); + onContentChanged?.(); + }, [onContentChanged]); + + const createWorkspaceCommand = useCallback( + (params: { + type: string; + label: string; + before: WorkspaceCommandState; + after: WorkspaceCommandState; + details?: Record; + }): WorkspaceCommand => + createSnapshotCommand({ + id: createId("command"), + type: params.type, + label: params.label, + before: params.before, + after: params.after, + details: params.details, + }), + [], + ); + + const executeWorkspaceCommand = useCallback( + (command: WorkspaceCommand) => { + const nextState = command.do(getCurrentCommandState()); + + applyCommandState(nextState); + setWorkspaceHistory((previous) => [ + ...previous, + toWorkspaceCommandRecord(command), + ]); + setRedoHistory([]); + markWorkspaceChanged(); + }, + [applyCommandState, getCurrentCommandState, markWorkspaceChanged], + ); + + const handleUndo = useCallback(() => { + const record = workspaceHistory[workspaceHistory.length - 1]; + if (!record) return; + + const command = reviveWorkspaceCommand(record); + const previousState = command.undo(getCurrentCommandState()); + + applyCommandState(previousState); + setWorkspaceHistory((previous) => previous.slice(0, -1)); + setRedoHistory((previous) => [...previous, record]); + markWorkspaceChanged(); + }, [ + applyCommandState, + getCurrentCommandState, + markWorkspaceChanged, + workspaceHistory, + ]); + + const handleRedo = useCallback(() => { + const record = redoHistory[redoHistory.length - 1]; + if (!record) return; + + const command = reviveWorkspaceCommand(record); + const nextState = command.do(getCurrentCommandState()); + + applyCommandState(nextState); + setRedoHistory((previous) => previous.slice(0, -1)); + setWorkspaceHistory((previous) => [...previous, record]); + markWorkspaceChanged(); + }, [ + applyCommandState, + getCurrentCommandState, + markWorkspaceChanged, + redoHistory, + ]); + + const replaceWorkspaceState = useCallback( + (state: ReplaceWorkspaceStateOptions = {}) => { + setPages(state.pages ?? []); + setSelectedPageIds(state.selectedPageIds ?? []); + setLastSelectedVisualIndex(state.lastSelectedVisualIndex ?? null); + setWorkspaceHistory(state.history ?? []); + setRedoHistory(state.redoHistory ?? []); + setWorkspaceDirty(state.dirty ?? false); + setWorkspaceMessage(state.message ?? null); + }, + [setLastSelectedVisualIndex, setPages, setSelectedPageIds], + ); + + const resetWorkspaceState = useCallback(() => { + replaceWorkspaceState(); + }, [replaceWorkspaceState]); + + return { + pages, + setPages, + selectedPageIds, + setSelectedPageIds, + lastSelectedVisualIndex, + setLastSelectedVisualIndex, + latestPagesRef, + + workspaceDirty, + setWorkspaceDirty, + workspaceMessage, + setWorkspaceMessage, + workspaceHistory, + setWorkspaceHistory, + redoHistory, + setRedoHistory, + + getCurrentCommandState, + applyCommandState, + createWorkspaceCommand, + executeWorkspaceCommand, + handleUndo, + handleRedo, + replaceWorkspaceState, + resetWorkspaceState, + }; +} diff --git a/src/workspace/workspaceCommands.test.ts b/src/workspace/workspaceCommands.test.ts new file mode 100644 index 0000000..f932ba4 --- /dev/null +++ b/src/workspace/workspaceCommands.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "vitest"; +import type { WorkspaceCommandState } from "./workspaceCommands"; +import { + cloneCommandState, + createSnapshotCommand, + reviveWorkspaceCommand, + toWorkspaceCommandRecord, +} from "./workspaceCommands"; + +function makeState(pageIds: string[]): WorkspaceCommandState { + return { + pages: pageIds.map((id, index) => ({ + id, + sourcePageIndex: index, + rotation: index * 90, + })), + selectedPageIds: pageIds.slice(0, 1), + lastSelectedVisualIndex: pageIds.length > 0 ? 0 : null, + }; +} + +describe("workspaceCommands", () => { + it("clones command state deeply enough for page and selection changes", () => { + const original = makeState(["a", "b"]); + const cloned = cloneCommandState(original); + + original.pages[0].rotation = 270; + original.selectedPageIds.push("b"); + original.lastSelectedVisualIndex = 1; + + expect(cloned).toEqual({ + pages: [ + { id: "a", sourcePageIndex: 0, rotation: 0 }, + { id: "b", sourcePageIndex: 1, rotation: 90 }, + ], + selectedPageIds: ["a"], + lastSelectedVisualIndex: 0, + }); + }); + + it("creates snapshot commands that are stable after source states mutate", () => { + const before = makeState(["a", "b"]); + const after = makeState(["b", "a"]); + after.selectedPageIds = ["b"]; + after.lastSelectedVisualIndex = 0; + + const command = createSnapshotCommand({ + id: "cmd-1", + type: "reorder-pages", + label: "Move page", + timestamp: "2026-05-17T10:00:00.000Z", + before, + after, + details: { moved: 1 }, + }); + + before.pages.length = 0; + after.pages[0].rotation = 180; + after.selectedPageIds.push("a"); + + expect(command.undo(makeState(["ignored"]))).toEqual({ + pages: [ + { id: "a", sourcePageIndex: 0, rotation: 0 }, + { id: "b", sourcePageIndex: 1, rotation: 90 }, + ], + selectedPageIds: ["a"], + lastSelectedVisualIndex: 0, + }); + + expect(command.do(makeState(["ignored"]))).toEqual({ + pages: [ + { id: "b", sourcePageIndex: 0, rotation: 0 }, + { id: "a", sourcePageIndex: 1, rotation: 90 }, + ], + selectedPageIds: ["b"], + lastSelectedVisualIndex: 0, + }); + }); + + it("round-trips commands through serializable records", () => { + const before = makeState(["a", "b", "c"]); + const after: WorkspaceCommandState = { + pages: [before.pages[2], before.pages[0], before.pages[1]], + selectedPageIds: ["c"], + lastSelectedVisualIndex: 0, + }; + + const command = createSnapshotCommand({ + id: "cmd-2", + type: "copy-pages", + label: "Copy pages", + timestamp: "2026-05-17T10:05:00.000Z", + before, + after, + }); + + const record = toWorkspaceCommandRecord(command); + const revived = reviveWorkspaceCommand(record); + + expect(record).not.toHaveProperty("do"); + expect(record).not.toHaveProperty("undo"); + expect(revived.do(before)).toEqual(after); + expect(revived.undo(after)).toEqual(before); + }); +}); diff --git a/src/workspace/workspaceCommands.ts b/src/workspace/workspaceCommands.ts index be024a5..7a66823 100644 --- a/src/workspace/workspaceCommands.ts +++ b/src/workspace/workspaceCommands.ts @@ -1,4 +1,4 @@ -import type { PageRef } from '../pdf/pdfTypes'; +import type { PageRef } from "../pdf/pdfTypes"; export interface WorkspaceCommandState { pages: PageRef[]; @@ -26,7 +26,7 @@ export interface WorkspaceCommand extends WorkspaceCommandRecord { } export function cloneCommandState( - state: WorkspaceCommandState + state: WorkspaceCommandState, ): WorkspaceCommandState { return { pages: state.pages.map((page) => ({ ...page })), @@ -58,7 +58,7 @@ export function createSnapshotCommand(params: { } export function reviveWorkspaceCommand( - record: WorkspaceCommandRecord + record: WorkspaceCommandRecord, ): WorkspaceCommand { return { ...record, @@ -68,7 +68,7 @@ export function reviveWorkspaceCommand( } export function toWorkspaceCommandRecord( - command: WorkspaceCommand + command: WorkspaceCommand, ): WorkspaceCommandRecord { return { id: command.id, @@ -81,4 +81,4 @@ export function toWorkspaceCommandRecord( details: command.payload.details, }, }; -} \ No newline at end of file +} diff --git a/src/workspace/workspaceDb.ts b/src/workspace/workspaceDb.ts index 34e122a..dc920c0 100644 --- a/src/workspace/workspaceDb.ts +++ b/src/workspace/workspaceDb.ts @@ -2,13 +2,13 @@ import type { LoadedWorkspace, StoredWorkspace, WorkspaceSummary, -} from './workspaceTypes'; +} from "./workspaceTypes"; -const DB_NAME = 'pdf-tools-workspaces'; +const DB_NAME = "pdf-tools-workspaces"; const DB_VERSION = 1; -const WORKSPACE_STORE = 'workspaces'; -const PDF_STORE = 'pdfBinaries'; +const WORKSPACE_STORE = "workspaces"; +const PDF_STORE = "pdfBinaries"; interface PdfBinaryRecord { pdfId: string; @@ -48,21 +48,21 @@ function openWorkspaceDb(): Promise { if (!db.objectStoreNames.contains(WORKSPACE_STORE)) { const workspaceStore = db.createObjectStore(WORKSPACE_STORE, { - keyPath: 'id', + keyPath: "id", }); - workspaceStore.createIndex('updatedAt', 'updatedAt', { + workspaceStore.createIndex("updatedAt", "updatedAt", { unique: false, }); - workspaceStore.createIndex('pdfId', 'pdfId', { + workspaceStore.createIndex("pdfId", "pdfId", { unique: false, }); } if (!db.objectStoreNames.contains(PDF_STORE)) { db.createObjectStore(PDF_STORE, { - keyPath: 'pdfId', + keyPath: "pdfId", }); } }; @@ -76,7 +76,7 @@ export async function listWorkspaces(): Promise { const db = await openWorkspaceDb(); try { - const tx = db.transaction(WORKSPACE_STORE, 'readonly'); + const tx = db.transaction(WORKSPACE_STORE, "readonly"); const store = tx.objectStore(WORKSPACE_STORE); const records = await requestToPromise(store.getAll()); @@ -113,13 +113,13 @@ export async function saveWorkspaceToIndexedDb({ const pdfRecord: PdfBinaryRecord = { pdfId: workspace.pdfId, name: workspace.pdfName, - blob: new Blob([pdfArrayBuffer], { type: 'application/pdf' }), + blob: new Blob([pdfArrayBuffer], { type: "application/pdf" }), size: pdfArrayBuffer.byteLength, createdAt: workspace.createdAt, updatedAt: now, }; - const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], 'readwrite'); + const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], "readwrite"); tx.objectStore(PDF_STORE).put(pdfRecord); tx.objectStore(WORKSPACE_STORE).put(workspace); @@ -131,15 +131,15 @@ export async function saveWorkspaceToIndexedDb({ } export async function loadWorkspaceFromIndexedDb( - workspaceId: string + workspaceId: string, ): Promise { const db = await openWorkspaceDb(); try { - const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], 'readonly'); + const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], "readonly"); const workspace = await requestToPromise( - tx.objectStore(WORKSPACE_STORE).get(workspaceId) + tx.objectStore(WORKSPACE_STORE).get(workspaceId), ); if (!workspace) { @@ -148,7 +148,7 @@ export async function loadWorkspaceFromIndexedDb( } const pdfRecord = await requestToPromise( - tx.objectStore(PDF_STORE).get(workspace.pdfId) + tx.objectStore(PDF_STORE).get(workspace.pdfId), ); await transactionDone(tx); @@ -169,20 +169,20 @@ export async function loadWorkspaceFromIndexedDb( } export async function deleteWorkspaceFromIndexedDb( - workspaceId: string + workspaceId: string, ): Promise { const db = await openWorkspaceDb(); try { - const lookupTx = db.transaction(WORKSPACE_STORE, 'readonly'); + const lookupTx = db.transaction(WORKSPACE_STORE, "readonly"); const workspace = await requestToPromise( - lookupTx.objectStore(WORKSPACE_STORE).get(workspaceId) + lookupTx.objectStore(WORKSPACE_STORE).get(workspaceId), ); await transactionDone(lookupTx); if (!workspace) return; - const deleteTx = db.transaction([WORKSPACE_STORE, PDF_STORE], 'readwrite'); + const deleteTx = db.transaction([WORKSPACE_STORE, PDF_STORE], "readwrite"); deleteTx.objectStore(WORKSPACE_STORE).delete(workspaceId); await transactionDone(deleteTx); @@ -190,14 +190,14 @@ export async function deleteWorkspaceFromIndexedDb( const remainingWorkspaces = await listWorkspaces(); const pdfStillUsed = remainingWorkspaces.some( - (summary) => summary.pdfId === workspace.pdfId + (summary) => summary.pdfId === workspace.pdfId, ); if (!pdfStillUsed) { const cleanupDb = await openWorkspaceDb(); try { - const cleanupTx = cleanupDb.transaction(PDF_STORE, 'readwrite'); + const cleanupTx = cleanupDb.transaction(PDF_STORE, "readwrite"); cleanupTx.objectStore(PDF_STORE).delete(workspace.pdfId); await transactionDone(cleanupTx); } finally { @@ -207,4 +207,4 @@ export async function deleteWorkspaceFromIndexedDb( } finally { db.close(); } -} \ No newline at end of file +} diff --git a/src/workspace/workspaceTypes.ts b/src/workspace/workspaceTypes.ts index 2c08077..924802a 100644 --- a/src/workspace/workspaceTypes.ts +++ b/src/workspace/workspaceTypes.ts @@ -1,5 +1,5 @@ -import type { PageRef } from '../pdf/pdfTypes'; -import type { WorkspaceCommandRecord } from './workspaceCommands'; +import type { PageRef } from "../pdf/pdfTypes"; +import type { WorkspaceCommandRecord } from "./workspaceCommands"; export interface StoredWorkspace { schemaVersion: 1; @@ -37,4 +37,4 @@ export interface WorkspaceSummary { export interface LoadedWorkspace { workspace: StoredWorkspace; pdfArrayBuffer: ArrayBuffer; -} \ No newline at end of file +} diff --git a/vite.config.ts b/vite.config.ts index 3cb33f8..6a2e0b8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,10 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react-swc'; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], server: { host: true, - allowedHosts: ['pdftools.add-ideas.de'], // ← ADD THIS + allowedHosts: ["pdftools.add-ideas.de"], // ← ADD THIS }, }); -