diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63b90d1..731d815 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 cache: npm @@ -37,6 +37,9 @@ jobs: - name: Run ESLint run: npm run lint + - name: Run docstring lint + run: npm run lint:docstrings + - name: Run TypeScript checks run: ./node_modules/.bin/tsc --noEmit @@ -57,7 +60,7 @@ jobs: - name: Upload test reports if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-reports if-no-files-found: ignore @@ -72,10 +75,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15582de..7a82622 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: repositories: ttdash - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ref: main @@ -99,7 +99,7 @@ jobs: fi - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 cache: npm @@ -138,6 +138,9 @@ jobs: - name: Run ESLint run: npm run lint + - name: Run docstring lint + run: npm run lint:docstrings + - name: Run TypeScript checks run: ./node_modules/.bin/tsc --noEmit @@ -157,7 +160,7 @@ jobs: run: npm run test:e2e:ci - name: Set up Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.4 @@ -172,7 +175,7 @@ jobs: - name: Load release signing secrets id: load-release-secrets - uses: 1Password/load-secrets-action@dafbe7cb03502b260e2b2893c753c352eee545bf # v3 + uses: 1Password/load-secrets-action@92467eb28f72e8255933372f1e0707c567ce2259 # v4.0.0 env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN_PUBLIC }} RELEASE_SIGNER_NAME: ${{ secrets.OP_SSH_BASE_URL }}name diff --git a/.gitignore b/.gitignore index 8ca452a..74a26da 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,12 @@ requirements/ /request-*.png /requests-*.png /ttdash-dashboard-*.png +/dashboard-*-review.png +/forecast-*-review.png +/help-*-review.png +/loaded-dashboard*.png +/mobile-*.png +/settings-*-review.png +/settings-empty.png +/tables-*-review.png +/empty-state.png diff --git a/.prettierrc.json b/.prettierrc.json index db1bfeb..90df20a 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,6 +4,8 @@ "semi": false, "trailingComma": "all", "printWidth": 100, + "tailwindStylesheet": "./src/index.css", + "plugins": ["prettier-plugin-tailwindcss"], "overrides": [ { "files": ["server.js", "usage-normalizer.js", "scripts/**/*.js", "server/**/*.js"], diff --git a/CHANGELOG.md b/CHANGELOG.md index bd17f50..38ab581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [6.2.1] - 2026-04-15 + +### Added + +- **Tiefere Drilldown-Analyse für `Letzte Tage`** — der Detaildialog zeigt jetzt deutlich mehr Tages- und Periodenkontext, darunter modellbezogene Kosten-, Token- und Request-Kennzahlen, Provider-Zusammenfassungen, Token-Verteilungen sowie Benchmarks gegen Vorperiode und Kurzzeitschnitt +- **Direkte Navigation im `Letzte Tage`-Drilldown** — innerhalb des geöffneten Detaildialogs kann jetzt direkt zum vorherigen oder nächsten Tag bzw. zur nächsten Periode gewechselt werden, inklusive Positionsanzeige und Pfeiltasten-Navigation +- **Verbindliche Docstring-Prüfung für Produktionscode** — kurze englische JSDoc-Kommentare werden jetzt für die öffentliche Produktionsoberfläche des Repos geprüft und lokal wie in CI/Release als eigener Gate mitgeführt + +### Improved + +- **Umfassende UI-Qualität im gesamten Dashboard** — Filter, Overlays, Toasts, Heatmaps, Tabellen, Karten und Diagrammflächen wurden nach einem tiefen UI-Review gezielt gehärtet, mit besserer Accessibility, klarerer Zustandskommunikation, stärkerer Mobile-Discoverability und konsistenteren Focus-/Zoom-Flows +- **Detailqualität und Aussagekraft der Dashboard-Ansichten** — Modell- und Provider-Informationen, Chart-Lesbarkeit, Light-/Dark-Parität, Filterstatus-Klarheit und mobile Header-/Legend-Darstellung wurden über mehrere Oberflächen hinweg präzisiert, ohne das bestehende Nutzungsmodell zu verändern +- **Performance auf Start-, Filter- und Großdatensatzpfaden** — der Dashboard-Root remountet bei normalen Filterwechseln nicht mehr unnötig, Bootstrap-Settings werden ohne sofortigen Doppel-Fetch wiederverwendet, zentrale Datenableitungen laufen gebündelter, und große Tabellen-/Sekundärflächen skalieren spürbar besser +- **Ladeverhalten und Chunking des Dashboards** — Settings, Help, Drilldown, Auto-Import und viele schwerere Analyse-Sektionen werden jetzt lazy geladen, wodurch der Initialpfad schlanker bleibt, bei unveränderter Funktionalität und mit gezielt angepasstem Animationsverhalten in einigen Dashboard-Sektionen +- **Lokale Runtime und Report-I/O** — Upload-, Settings- und PDF-/Report-Pfade blockieren den Event Loop weniger stark, weil mehrere synchrone Dateisystemoperationen auf asynchronere Verarbeitung umgestellt wurden +- **Absicherung für die Weiterentwicklung** — neue und erweiterte Frontend-, Hook-, Daten- und E2E-Tests decken die UI-, Drilldown- und Performance-Verbesserungen gezielt ab +- **Lint-, Format- und Test-Gates für die Weiterentwicklung** — React-, Accessibility-, Import-, Testing-Library-, jest-dom- und Playwright-Linting sowie Tailwind-Class-Sorting sind jetzt Teil der normalen lokalen und GitHub-Gates +- **Release- und Maintainer-Dokumentation** — `RELEASING.md` beschreibt jetzt Fehlerszenarien und Retry-Bedingungen klarer, einschließlich 1Password-URL-Validierung und GitHub-Domain-Verifikation +- **Workflow-Pins und Build-Tooling** — GitHub Actions sind auf aktuelle stabile Commit-Hashes samt präziser Versionskommentare gebracht, und kompatible Direktabhängigkeiten wie `react-i18next` und `typescript-eslint` wurden aktualisiert + +### Fixed + +- **Semantik und Bedienbarkeit zentraler Filter- und Overlay-Flächen** — Date-Picker, Filter-Chips, Info-Buttons und Toasts verhalten sich jetzt konsistenter für Keyboard-, Screenreader- und Touch-Nutzung +- **Bewegungs- und Diagrammverhalten in Dashboard-Sektionen** — doppelte oder unpassende Reveal-/Chart-Animationen, unvollständige Reduced-Motion-Pfade und mehrere Timing-/Discoverability-Probleme in expandierbaren Analyseflächen wurden bereinigt +- **Skalierungsprobleme in `Letzte Tage` und sekundären Oberflächen** — große Tabellenansichten, Help-/Settings-Öffnung und weitere schwere UI-Pfade reagieren unter größeren Datenmengen robuster als zuvor +- **Mehrere Review- und CI-Findings aus CodeRabbit und lokaler Validierung** — period-aware Drilldown-Benchmarks, Heatmap-Semantik, Cache-ROI-Vorzeichenlogik, Help-Dialog-Lifecycle, Weekday-Lokalisierung, testbezogene Timer-/RAF-Stabilität und race-sichere serverseitige Datei-Mutationen wurden gezielt bereinigt + ## [6.2.0] - 2026-04-14 ### Added diff --git a/RELEASING.md b/RELEASING.md index afe0ee5..169ab4e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,10 +12,10 @@ Before the first public release, configure npm Trusted Publishing for this repos 6. Install the `ttdash-release` GitHub App on `roastcodes/ttdash` 7. Add `APP_CLIENT_ID` and `APP_PRIVATE_KEY` as Actions secrets for this repository or the `release` environment 8. Add `OP_SERVICE_ACCOUNT_TOKEN_PUBLIC` as an Actions secret for this repository or the `release` environment -9. Add `OP_SSH_BASE_URL` as an Actions secret for this repository or the `release` environment, point it to the shared 1Password item prefix for the release signer, and make sure it ends with a trailing `/` +9. Add `OP_SSH_BASE_URL` as an Actions secret for this repository or the `release` environment, point it to the shared 1Password item prefix for the release signer, and make sure it ends with a trailing `/` (otherwise the workflow fails its format-validation step before it attempts to load secrets) 10. Add the `ttdash-release` GitHub App as a bypass actor in the `main` ruleset -The release workflow loads the SSH signing identity from 1Password through the public-repo service account token. `OP_SSH_BASE_URL` must contain only the common item prefix and must end with `/`, for example `op://vault/item/`, while the workflow appends `name`, `comment`, `public key`, and `private key?ssh-format=openssh` internally. The workflow validates this format before loading secrets. The SSH public key must remain added to the maintainer GitHub account as an SSH signing key, and the signing email used by the workflow must stay valid for both GitHub verification and the `roastcodes` organization trailer. +The release workflow loads the SSH signing identity from 1Password through the public-repo service account token. `OP_SSH_BASE_URL` must contain only the common item prefix and must end with `/`, for example `op://vault/item/`, while the workflow appends `name`, `comment`, `public key`, and `private key?ssh-format=openssh` internally. The workflow validates this format before loading secrets and exits early with a format error if the trailing slash is missing. The SSH public key must remain added to the maintainer GitHub account as an SSH signing key, and the signing email used by the workflow must stay valid for both GitHub verification and the `roastcodes` organization trailer. Trusted Publishing is preferred because it avoids long-lived npm tokens and enables provenance for public publishes. @@ -28,7 +28,7 @@ Before using the manual release workflow, make sure: 1. `main` is protected and requires the `CI` status check before merges 2. CodeQL is enabled in the GitHub UI if you want it as a manual release gate 3. the `ttdash-release` GitHub App is allowed to push the version-bump commit and signed tag back to `main` -4. the `roast.codes` domain remains verified for the `roastcodes` organization so the workflow-created `on-behalf-of: @roastcodes ` trailer continues to render correctly on GitHub +4. the `roast.codes` domain remains verified for the `roastcodes` organization so the workflow-created `on-behalf-of: @roastcodes ` trailer continues to render correctly on GitHub (check GitHub organization settings under verified domains; if verification lapses, restore or confirm the DNS TXT record and re-verify the domain as documented at https://docs.github.com/en/organizations/managing-organization-settings/verifying-or-approving-a-domain-for-your-organization) If branch protection or rulesets block the `ttdash-release` app from writing to `main` or pushing `v*` tags, the workflow will fail when it tries to push the release commit or tag. @@ -71,7 +71,13 @@ On a manual `workflow_dispatch` run against `main`, the workflow: 14. creates the GitHub release Note: the workflow reruns the release-critical test suite itself after the version bump. This is necessary because the workflow-created push back to `main` should not be relied on to trigger the normal `CI` workflow again. -If a release fails after the version bump was already pushed, rerunning the workflow with the same version only resumes that release while `main` still points at the original `vX.Y.Z: Release` commit. Retry mode also requires any pre-existing `vX.Y.Z` tag to already be signed and to point at that same release commit. If new commits landed on `main` in the meantime, or an existing tag does not match the release commit, the workflow now aborts early and you should cut a new version instead of retrying the old one. +If a release fails after the version bump was already pushed, rerunning the workflow with the same version resumes that release only when all retry conditions still hold: + +- `main` still points at the original `vX.Y.Z: Release` commit +- any pre-existing `vX.Y.Z` tag is already signed +- any pre-existing `vX.Y.Z` tag points at that same release commit + +If new commits landed on `main` in the meantime, or an existing tag does not match the release commit, the workflow aborts early and you should cut a new version instead of retrying the old one. ## Post-Publish Checks diff --git a/eslint.config.mjs b/eslint.config.mjs index 5ff6928..e00c7fe 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,15 @@ import { defineConfig } from 'eslint/config' import js from '@eslint/js' import eslintConfigPrettier from 'eslint-config-prettier' +import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript' +import importPlugin from 'eslint-plugin-import-x' +import jestDom from 'eslint-plugin-jest-dom' +import jsdoc from 'eslint-plugin-jsdoc' +import jsxA11y from 'eslint-plugin-jsx-a11y' +import playwright from 'eslint-plugin-playwright' +import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' +import testingLibrary from 'eslint-plugin-testing-library' import globals from 'globals' import tseslint from 'typescript-eslint' @@ -92,5 +100,185 @@ export default defineConfig( '@typescript-eslint/switch-exhaustiveness-check': 'error', }, }, + { + files: ['src/**/*.{tsx,jsx}'], + extends: [react.configs.flat.recommended, react.configs.flat['jsx-runtime']], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + settings: { + react: { + version: 'detect', + }, + }, + }, + { + files: ['src/**/*.{tsx,jsx}'], + extends: [jsxA11y.flatConfigs.recommended], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }, + { + files: ['tests/frontend/**/*.test.tsx'], + extends: [testingLibrary.configs['flat/react'], jestDom.configs['flat/recommended']], + rules: { + 'testing-library/no-container': 'off', + 'testing-library/no-node-access': 'off', + }, + }, + { + files: ['tests/e2e/**/*.ts'], + extends: [playwright.configs['flat/recommended']], + }, + { + files: ['**/*.{js,cjs,mjs,ts,tsx}'], + extends: [importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.typescript], + languageOptions: { + ecmaVersion: 'latest', + }, + settings: { + 'import-x/resolver-next': [ + createTypeScriptImportResolver({ + alwaysTryTypes: true, + project: './tsconfig.json', + }), + ], + }, + rules: { + 'import-x/export': 'error', + 'import-x/first': 'error', + 'import-x/newline-after-import': 'error', + 'import-x/no-duplicates': 'error', + 'import-x/no-named-as-default': 'off', + 'import-x/no-named-as-default-member': 'off', + 'import-x/no-unresolved': 'error', + }, + }, + { + files: ['src/**/*.{ts,tsx}', 'shared/**/*.d.ts'], + plugins: { + jsdoc, + }, + settings: { + jsdoc: { + mode: 'typescript', + }, + }, + rules: { + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-syntax': 'error', + 'jsdoc/check-tag-names': 'error', + 'jsdoc/empty-tags': 'error', + 'jsdoc/no-types': 'error', + 'jsdoc/require-description': [ + 'error', + { + checkConstructors: false, + contexts: [ + 'TSInterfaceDeclaration', + 'TSTypeAliasDeclaration', + 'FunctionDeclaration', + 'VariableDeclaration', + 'ClassDeclaration', + ], + descriptionStyle: 'body', + }, + ], + 'jsdoc/require-description-complete-sentence': 'error', + 'jsdoc/require-hyphen-before-param-description': ['error', 'always'], + 'jsdoc/require-jsdoc': [ + 'error', + { + checkConstructors: false, + checkGetters: false, + checkSetters: false, + contexts: [ + 'ExportNamedDeclaration > TSInterfaceDeclaration', + 'ExportNamedDeclaration > TSTypeAliasDeclaration', + 'ExportNamedDeclaration > VariableDeclaration', + ], + publicOnly: { + ancestorsOnly: true, + esm: true, + }, + require: { + ArrowFunctionExpression: false, + ClassDeclaration: true, + FunctionDeclaration: true, + FunctionExpression: false, + MethodDefinition: false, + }, + }, + ], + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-returns-type': 'off', + 'jsdoc/sort-tags': 'error', + }, + }, + { + files: ['shared/**/*.js', 'server/**/*.js', 'server.js', 'usage-normalizer.js'], + plugins: { + jsdoc, + }, + settings: { + jsdoc: { + mode: 'typescript', + }, + }, + rules: { + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-syntax': 'error', + 'jsdoc/check-tag-names': 'error', + 'jsdoc/empty-tags': 'error', + 'jsdoc/no-types': 'error', + 'jsdoc/require-description': [ + 'error', + { + checkConstructors: false, + contexts: ['FunctionDeclaration', 'VariableDeclaration'], + descriptionStyle: 'body', + }, + ], + 'jsdoc/require-description-complete-sentence': 'error', + 'jsdoc/require-hyphen-before-param-description': ['error', 'always'], + 'jsdoc/require-jsdoc': [ + 'error', + { + checkConstructors: false, + checkGetters: false, + checkSetters: false, + contexts: ['ExportNamedDeclaration > VariableDeclaration'], + publicOnly: { + ancestorsOnly: true, + cjs: true, + esm: true, + }, + require: { + ArrowFunctionExpression: false, + ClassDeclaration: true, + FunctionDeclaration: true, + FunctionExpression: false, + MethodDefinition: false, + }, + }, + ], + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-returns-type': 'off', + 'jsdoc/sort-tags': 'error', + }, + }, eslintConfigPrettier, ) diff --git a/package-lock.json b/package-lock.json index c46de93..2ca0a45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "cross-spawn": "^7.0.6", "i18next": "^26.0.3", - "react-i18next": "^17.0.2", + "react-i18next": "^17.0.3", "react-is": "^19.2.4" }, "bin": { @@ -38,19 +38,28 @@ "cmdk": "^1.1.1", "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import-x": "^4.16.2", + "eslint-plugin-jest-dom": "^5.5.0", + "eslint-plugin-jsdoc": "^62.9.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-playwright": "^2.10.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-testing-library": "^7.16.2", "framer-motion": "^12.6.5", "globals": "^17.5.0", "jsdom": "^29.0.2", "lucide-react": "^1.7.0", "prettier": "^3.8.2", + "prettier-plugin-tailwindcss": "^0.7.2", "react": "^19.2.4", "react-dom": "^19.2.4", "recharts": "^3.8.1", "tailwind-merge": "^3.0.2", "tailwindcss": "^4.1.3", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", + "typescript-eslint": "^8.58.2", "vite": "^8.0.8", "vitest": "^4.1.3" }, @@ -580,6 +589,33 @@ "tslib": "^2.4.0" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz", + "integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.58.0", + "comment-parser": "1.4.6", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "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", @@ -928,6 +964,13 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@package-json/types": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz", + "integrity": "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==", + "dev": true, + "license": "MIT" + }, "node_modules/@playwright/test": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", @@ -1995,6 +2038,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2536,17 +2592,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", - "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/type-utils": "8.58.1", - "@typescript-eslint/utils": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2559,7 +2615,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.58.1", + "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2575,16 +2631,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", - "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "engines": { @@ -2600,14 +2656,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", - "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.1", - "@typescript-eslint/types": "^8.58.1", + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "engines": { @@ -2622,14 +2678,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", - "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1" + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2640,9 +2696,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", - "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", "dev": true, "license": "MIT", "engines": { @@ -2657,15 +2713,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", - "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2682,9 +2738,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", - "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", "dev": true, "license": "MIT", "engines": { @@ -2696,16 +2752,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", - "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.1", - "@typescript-eslint/tsconfig-utils": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2763,16 +2819,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", - "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1" + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2787,13 +2843,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", - "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2817,6 +2873,312 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -3052,6 +3414,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3082,28 +3454,173 @@ "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==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -3111,6 +3628,52 @@ "dev": true, "license": "MIT" }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz", + "integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3186,6 +3749,56 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3320,6 +3933,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comment-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", + "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3508,6 +4131,13 @@ "node": ">=12" } }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -3522,6 +4152,60 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3561,6 +4245,42 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3588,6 +4308,19 @@ "dev": true, "license": "MIT" }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -3596,6 +4329,21 @@ "license": "MIT", "peer": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.335", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", @@ -3603,6 +4351,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -3630,54 +4385,231 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/es-toolkit": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", - "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] + "engines": { + "node": ">= 0.4" + } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "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==", + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.4" } }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, - "license": "MIT", - "dependencies": { + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "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": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", @@ -3747,153 +4679,491 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", "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" + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" }, "engines": { - "node": ">=18" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", "dev": true, - "license": "BSD-2-Clause", + "license": "ISC", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.17.0 || >=18.6.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/eslint-plugin-import-x": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz", + "integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@package-json/types": "^0.0.12", + "@typescript-eslint/types": "^8.56.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.1.2", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/eslint-plugin-import-x/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": "BSD-2-Clause", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "18 || 20 || >=22" } }, - "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==", + "node_modules/eslint-plugin-import-x/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": "BSD-3-Clause", + "license": "BlueOak-1.0.0", "dependencies": { - "estraverse": "^5.1.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=0.10" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/eslint-plugin-jest-dom": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.5.0.tgz", + "integrity": "sha512-CRlXfchTr7EgC3tDI7MGHY6QjdJU5Vv2RPaeeGtkXUHnKZf04kgzMPIJUXt4qKCvYWVVIEo9ut9Oq1vgXAykEA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "@babel/runtime": "^7.16.3", + "requireindex": "^1.2.0" }, "engines": { - "node": ">=4.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0", + "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/dom": { + "optional": true + } } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/eslint-plugin-jsdoc": { + "version": "62.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz", + "integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==", "dev": true, - "license": "BSD-2-Clause", + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.86.0", + "@es-joy/resolve.exports": "1.2.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.6", + "debug": "^4.4.3", + "escape-string-regexp": "^4.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.0", + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.4", + "spdx-expression-parse": "^4.0.0", + "to-valid-identifier": "^1.0.0" + }, "engines": { - "node": ">=4.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.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==", + "node_modules/eslint-plugin-jsdoc/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": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/eslint-plugin-jsdoc/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": ">=0.10.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } }, - "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==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12.0.0" + "node": ">= 0.4" } }, - "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==", + "node_modules/eslint-plugin-playwright": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.10.1.tgz", + "integrity": "sha512-qea3UxBOb8fTwJ77FMApZKvRye5DOluDHcev0LDJwID3RELeun0JlqzrNIXAB/SXCyB/AesCW/6sZfcT9q3Edg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "globals": "^17.3.0" + }, + "engines": { + "node": ">=16.9.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "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" + } + }, + "node_modules/eslint-plugin-react/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/eslint-plugin-testing-library": { + "version": "7.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.16.2.tgz", + "integrity": "sha512-8gleGnQXK2ZA3hHwjCwpYTZvM+9VsrJ+/9kDI8CjqAQGAdMQOdn/rJNu7ZySENuiWlGKQWyZJ4ZjEg2zamaRHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.56.0", + "@typescript-eslint/utils": "^8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "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", @@ -3978,6 +5248,22 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/framer-motion": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", @@ -4018,244 +5304,887 @@ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.4.tgz", + "integrity": "sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "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": ">=6" + "node": ">=0.10.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==", + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "call-bound": "^1.0.3" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "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": ">=8" + "node": ">=0.10.0" } }, - "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==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "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==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "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/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/i18next": { - "version": "26.0.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.4.tgz", - "integrity": "sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA==", - "funding": [ - { - "type": "individual", - "url": "https://www.locize.com/i18next" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - }, - { - "type": "individual", - "url": "https://www.locize.com" - } - ], + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.29.2" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, - "peerDependencies": { - "typescript": "^5 || ^6" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=6" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, "engines": { - "node": ">=0.8.19" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, @@ -4304,6 +6233,24 @@ "node": ">=8" } }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -4334,6 +6281,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", + "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/jsdom": { "version": "29.0.2", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", @@ -4422,6 +6379,22 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4432,6 +6405,26 @@ "json-buffer": "3.0.1" } }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4730,6 +6723,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "11.3.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", @@ -4799,6 +6805,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -4872,6 +6888,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4879,6 +6911,35 @@ "dev": true, "license": "MIT" }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/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/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -4886,6 +6947,121 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-deep-merge": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.0.tgz", + "integrity": "sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -4915,6 +7091,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4960,6 +7154,23 @@ "node": ">=6" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -4992,6 +7203,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5066,6 +7284,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -5121,6 +7349,85 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz", + "integrity": "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -5145,6 +7452,25 @@ "license": "MIT", "peer": true }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5178,9 +7504,9 @@ } }, "node_modules/react-i18next": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz", - "integrity": "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.3.tgz", + "integrity": "sha512-x4xjvUNZ56T+zfXWNedNnCET9Xq1IBYWX7IsWo5cCQ/RT+Rm7GWqt0h9PShFi4IhyMnsdiu1C6Jc4DE+/S3PFQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", @@ -5368,6 +7694,50 @@ "redux": "^5.0.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5378,6 +7748,16 @@ "node": ">=0.10.0" } }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -5385,6 +7765,43 @@ "dev": true, "license": "MIT" }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5395,6 +7812,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", @@ -5436,6 +7863,61 @@ "dev": true, "license": "MIT" }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -5469,6 +7951,55 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5490,6 +8021,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -5507,6 +8114,41 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5521,6 +8163,133 @@ "dev": true, "license": "MIT" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -5560,6 +8329,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -5670,6 +8452,23 @@ "dev": true, "license": "MIT" }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -5729,6 +8528,84 @@ "node": ">= 0.8.0" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", @@ -5744,16 +8621,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", - "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", + "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.1", - "@typescript-eslint/parser": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/utils": "8.58.1" + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5767,6 +8644,25 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", @@ -5777,6 +8673,41 @@ "node": ">=20.18.1" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6135,6 +9066,95 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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", diff --git a/package.json b/package.json index 1e9df36..6019655 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "format": "prettier . --write", "format:check": "prettier . --check", "lint": "eslint .", + "lint:docstrings": "eslint src server.js usage-normalizer.js shared --ext .ts,.tsx,.js,.d.ts", "lint:fix": "eslint . --fix", "preview": "vite preview", "start": "node server.js", @@ -34,10 +35,10 @@ "test:all": "npm run test:unit && npm run test:e2e", "docs:screenshots": "node scripts/capture-readme-screenshots.js", "pack:dry-run": "npm pack --dry-run", - "verify": "npm run format:check && npm run lint && tsc --noEmit && npm run test:unit && npm run build:app && npm run verify:package", + "verify": "npm run format:check && npm run lint && npm run lint:docstrings && tsc --noEmit && npm run test:unit && npm run build:app && npm run verify:package", "verify:package": "node scripts/verify-package.js", "verify:registry-install": "node scripts/verify-registry-install.js", - "verify:release": "npm run format:check && npm run lint && tsc --noEmit && npm run test:unit:coverage && npm run build:app && npm run verify:package", + "verify:release": "npm run format:check && npm run lint && npm run lint:docstrings && tsc --noEmit && npm run test:unit:coverage && npm run build:app && npm run verify:package", "prepare": "npm run build:app" }, "files": [ @@ -87,26 +88,35 @@ "cmdk": "^1.1.1", "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import-x": "^4.16.2", + "eslint-plugin-jest-dom": "^5.5.0", + "eslint-plugin-jsdoc": "^62.9.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-playwright": "^2.10.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-testing-library": "^7.16.2", "framer-motion": "^12.6.5", "globals": "^17.5.0", "jsdom": "^29.0.2", "lucide-react": "^1.7.0", "prettier": "^3.8.2", + "prettier-plugin-tailwindcss": "^0.7.2", "react": "^19.2.4", "react-dom": "^19.2.4", "recharts": "^3.8.1", "tailwind-merge": "^3.0.2", "tailwindcss": "^4.1.3", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", + "typescript-eslint": "^8.58.2", "vite": "^8.0.8", "vitest": "^4.1.3" }, "dependencies": { "cross-spawn": "^7.0.6", "i18next": "^26.0.3", - "react-i18next": "^17.0.2", + "react-i18next": "^17.0.3", "react-is": "^19.2.4" } } diff --git a/server.js b/server.js index 4ac7078..0cb5592 100755 --- a/server.js +++ b/server.js @@ -2,6 +2,7 @@ const http = require('http'); const fs = require('fs'); +const fsPromises = require('fs/promises'); const os = require('os'); const path = require('path'); const readline = require('readline/promises'); @@ -271,6 +272,9 @@ const BACKGROUND_LOG_DIR = path.join(APP_PATHS.cacheDir, 'background'); const BACKGROUND_INSTANCES_LOCK_DIR = path.join(APP_PATHS.configDir, 'background-instances.lock'); const BACKGROUND_INSTANCES_LOCK_TIMEOUT_MS = 5000; const BACKGROUND_INSTANCES_LOCK_STALE_MS = 10000; +const FILE_MUTATION_LOCK_TIMEOUT_MS = 10000; +const FILE_MUTATION_LOCK_STALE_MS = 30000; +const fileMutationLocks = new Map(); const MIME_TYPES = { '.html': 'text/html; charset=utf-8', @@ -314,6 +318,214 @@ function writeJsonAtomic(filePath, data) { fs.renameSync(tempPath, filePath); } +async function writeJsonAtomicAsync(filePath, data) { + const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; + let tempPathCreated = false; + + try { + await fsPromises.mkdir(path.dirname(filePath), { recursive: true, mode: SECURE_DIR_MODE }); + tempPathCreated = true; + await fsPromises.writeFile(tempPath, JSON.stringify(data, null, 2), { + mode: SECURE_FILE_MODE, + }); + + if (!IS_WINDOWS) { + await fsPromises.chmod(tempPath, SECURE_FILE_MODE); + } + + await fsPromises.rename(tempPath, filePath); + } catch (error) { + if (tempPathCreated) { + try { + await fsPromises.unlink(tempPath); + } catch (unlinkError) { + if (unlinkError?.code !== 'ENOENT') { + // Ignore temp-file cleanup failures so the original error wins. + } + } + } + throw error; + } +} + +async function unlinkIfExists(filePath) { + try { + await fsPromises.unlink(filePath); + } catch (error) { + if (error?.code !== 'ENOENT') { + throw error; + } + } +} + +function getFileMutationLockDir(filePath) { + return `${filePath}.lock`; +} + +function getFileMutationLockOwnerPath(lockDir) { + return path.join(lockDir, 'owner.json'); +} + +async function removeFileMutationLockDir(lockDir) { + try { + await fsPromises.rm(lockDir, { recursive: true, force: true }); + } catch (error) { + if (error?.code !== 'ENOENT') { + throw error; + } + } +} + +async function writeFileMutationLockOwner(lockDir) { + const ownerPath = getFileMutationLockOwnerPath(lockDir); + const owner = { + pid: process.pid, + createdAt: new Date().toISOString(), + instanceId: RUNTIME_INSTANCE.id, + }; + await fsPromises.writeFile(ownerPath, JSON.stringify(owner, null, 2), { + mode: SECURE_FILE_MODE, + }); + if (!IS_WINDOWS) { + await fsPromises.chmod(ownerPath, SECURE_FILE_MODE); + } +} + +async function shouldReapFileMutationLock(lockDir) { + const ownerPath = getFileMutationLockOwnerPath(lockDir); + let owner = null; + + try { + const rawOwner = await fsPromises.readFile(ownerPath, 'utf-8'); + owner = JSON.parse(rawOwner); + } catch (error) { + if (error?.code !== 'ENOENT') { + // Fall back to age-based cleanup if the owner metadata is missing or malformed. + } + } + + try { + const ownerCreatedAt = owner?.createdAt ? Date.parse(owner.createdAt) : Number.NaN; + const stats = await fsPromises.stat(lockDir); + const lockAgeMs = Number.isFinite(ownerCreatedAt) + ? Date.now() - ownerCreatedAt + : Date.now() - stats.mtimeMs; + + if (lockAgeMs > FILE_MUTATION_LOCK_STALE_MS) { + return true; + } + + if (Number.isInteger(owner?.pid)) { + return !isProcessRunning(owner.pid); + } + + return false; + } catch (error) { + if (error?.code === 'ENOENT') { + return false; + } + throw error; + } +} + +async function withCrossProcessFileMutationLock( + filePath, + operation, + timeoutMs = FILE_MUTATION_LOCK_TIMEOUT_MS, +) { + const lockDir = getFileMutationLockDir(filePath); + const startedAt = Date.now(); + + while (true) { + try { + await fsPromises.mkdir(path.dirname(lockDir), { + recursive: true, + mode: SECURE_DIR_MODE, + }); + await fsPromises.mkdir(lockDir, { mode: SECURE_DIR_MODE }); + if (!IS_WINDOWS) { + await fsPromises.chmod(lockDir, SECURE_DIR_MODE); + } + + try { + await writeFileMutationLockOwner(lockDir); + } catch (error) { + await removeFileMutationLockDir(lockDir).catch(() => undefined); + throw error; + } + + break; + } catch (error) { + if (!error || error.code !== 'EEXIST') { + throw error; + } + + if (await shouldReapFileMutationLock(lockDir)) { + await removeFileMutationLockDir(lockDir).catch(() => undefined); + continue; + } + + if (Date.now() - startedAt >= timeoutMs) { + throw new Error(`Could not acquire file mutation lock for ${path.basename(filePath)}.`, { + cause: error, + }); + } + + await sleep(50); + } + } + + try { + return await operation(); + } finally { + try { + await removeFileMutationLockDir(lockDir); + } catch { + // Ignore cleanup races so the original operation result wins. + } + } +} + +async function withFileMutationLock(filePath, operation) { + const previous = fileMutationLocks.get(filePath) || Promise.resolve(); + let releaseCurrent; + const current = new Promise((resolve) => { + releaseCurrent = resolve; + }); + + fileMutationLocks.set(filePath, current); + + await previous.catch(() => undefined); + + try { + return await withCrossProcessFileMutationLock(filePath, operation); + } finally { + releaseCurrent(); + if (fileMutationLocks.get(filePath) === current) { + fileMutationLocks.delete(filePath); + } + } +} + +async function withOrderedFileMutationLocks(filePaths, operation) { + const uniquePaths = Array.from(new Set(filePaths)).sort(); + + const runWithLock = async (index) => { + if (index >= uniquePaths.length) { + return operation(); + } + + const filePath = uniquePaths[index]; + return withFileMutationLock(filePath, () => runWithLock(index + 1)); + }; + + return runWithLock(0); +} + +async function withSettingsAndDataMutationLock(operation) { + return withOrderedFileMutationLocks([SETTINGS_FILE, DATA_FILE], operation); +} + function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -1389,8 +1601,8 @@ function readData() { } } -function writeData(data) { - writeJsonAtomic(DATA_FILE, data); +async function writeData(data) { + await writeJsonAtomicAsync(DATA_FILE, data); } function readSettings() { @@ -1420,53 +1632,43 @@ function readSettingsForWrite() { } } -function writeSettings(settings) { - writeJsonAtomic(SETTINGS_FILE, normalizeSettings(settings)); +async function writeSettings(settings) { + await writeJsonAtomicAsync(SETTINGS_FILE, normalizeSettings(settings)); } -function updateSettings(patch) { +async function updateDataLoadState(patch) { const current = readSettingsForWrite(); const next = { ...current, - ...(patch && typeof patch === 'object' ? patch : {}), + ...patch, }; - if (patch && Object.prototype.hasOwnProperty.call(patch, 'providerLimits')) { - next.providerLimits = normalizeProviderLimits(patch.providerLimits); - } else { - next.providerLimits = current.providerLimits; - } - - next.language = normalizeLanguage(next.language); - next.theme = normalizeTheme(next.theme); - - writeSettings(next); + await writeSettings(next); return toSettingsResponse(next); } -function recordDataLoad(source) { - const current = readSettingsForWrite(); - const next = { - ...current, - lastLoadedAt: new Date().toISOString(), - lastLoadSource: source, - }; +async function updateSettings(patch) { + return withFileMutationLock(SETTINGS_FILE, async () => { + const current = readSettingsForWrite(); + const next = { + ...current, + ...(patch && typeof patch === 'object' ? patch : {}), + }; - writeSettings(next); - return toSettingsResponse(next); -} + if (patch && Object.prototype.hasOwnProperty.call(patch, 'providerLimits')) { + next.providerLimits = normalizeProviderLimits(patch.providerLimits); + } else { + next.providerLimits = current.providerLimits; + } -function clearDataLoadState() { - const current = readSettingsForWrite(); - const next = { - ...current, - lastLoadedAt: null, - lastLoadSource: null, - }; + next.language = normalizeLanguage(next.language); + next.theme = normalizeTheme(next.theme); - writeSettings(next); - return toSettingsResponse(next); + await writeSettings(next); + return toSettingsResponse(next); + }); } + const { json, readBody, resolveApiPath, sendBuffer, validateMutationRequest } = createHttpUtils({ apiPrefix: API_PREFIX, maxBodySize: MAX_BODY_SIZE, @@ -1647,8 +1849,13 @@ async function performAutoImport({ }); const normalized = normalizeIncomingData(JSON.parse(rawJson)); - writeData(normalized); - recordDataLoad(source); + await withSettingsAndDataMutationLock(async () => { + await writeData(normalized); + await updateDataLoadState({ + lastLoadedAt: new Date().toISOString(), + lastLoadSource: source, + }); + }); return { days: normalized.daily.length, @@ -1743,12 +1950,13 @@ const server = http.createServer(async (req, res) => { if (validationError) { return json(res, validationError.status, { message: validationError.message }); } - try { - fs.unlinkSync(DATA_FILE); - } catch { - // Ignore missing data files during reset. - } - clearDataLoadState(); + await withSettingsAndDataMutationLock(async () => { + await unlinkIfExists(DATA_FILE); + await updateDataLoadState({ + lastLoadedAt: null, + lastLoadSource: null, + }); + }); return json(res, 200, { success: true }); } return json(res, 405, { message: 'Method Not Allowed' }); @@ -1786,11 +1994,9 @@ const server = http.createServer(async (req, res) => { if (validationError) { return json(res, validationError.status, { message: validationError.message }); } - try { - fs.unlinkSync(SETTINGS_FILE); - } catch { - // Ignore missing settings files during reset. - } + await withFileMutationLock(SETTINGS_FILE, async () => { + await unlinkIfExists(SETTINGS_FILE); + }); return json(res, 200, { success: true, settings: readSettings() }); } @@ -1801,7 +2007,7 @@ const server = http.createServer(async (req, res) => { } try { const body = await readBody(req); - return json(res, 200, updateSettings(body)); + return json(res, 200, await updateSettings(body)); } catch (e) { if (isPayloadTooLargeError(e)) { return json(res, 413, { message: 'Settings request too large' }); @@ -1826,7 +2032,9 @@ const server = http.createServer(async (req, res) => { try { const body = await readBody(req); const importedSettings = normalizeSettings(extractSettingsImportPayload(body)); - writeSettings(importedSettings); + await withFileMutationLock(SETTINGS_FILE, async () => { + await writeSettings(importedSettings); + }); return json(res, 200, toSettingsResponse(importedSettings)); } catch (e) { if (isPayloadTooLargeError(e)) { @@ -1846,8 +2054,13 @@ const server = http.createServer(async (req, res) => { try { const body = await readBody(req); const normalized = normalizeIncomingData(body); - writeData(normalized); - recordDataLoad('file'); + await withSettingsAndDataMutationLock(async () => { + await writeData(normalized); + await updateDataLoadState({ + lastLoadedAt: new Date().toISOString(), + lastLoadSource: 'file', + }); + }); const days = normalized.daily.length; const totalCost = normalized.totals.totalCost; return json(res, 200, { days, totalCost }); @@ -1875,10 +2088,16 @@ const server = http.createServer(async (req, res) => { try { const body = await readBody(req); const importedData = normalizeIncomingData(extractUsageImportPayload(body)); - const currentData = readData(); - const result = mergeUsageData(currentData, importedData); - writeData(result.data); - recordDataLoad('file'); + const result = await withSettingsAndDataMutationLock(async () => { + const currentData = readData(); + const merged = mergeUsageData(currentData, importedData); + await writeData(merged.data); + await updateDataLoadState({ + lastLoadedAt: new Date().toISOString(), + lastLoadSource: 'file', + }); + return merged; + }); return json(res, 200, result.summary); } catch (e) { if (isPayloadTooLargeError(e)) { @@ -2096,6 +2315,12 @@ module.exports = { commandExists, getExecutableName, listenOnAvailablePort, + getFileMutationLockDir, + unlinkIfExists, + writeJsonAtomicAsync, + withFileMutationLock, + withOrderedFileMutationLocks, + getPendingFileMutationLockCount: () => fileMutationLocks.size, }, }; diff --git a/server/report/index.js b/server/report/index.js index 442b7b3..2d4c889 100644 --- a/server/report/index.js +++ b/server/report/index.js @@ -1,4 +1,4 @@ -const fs = require('fs'); +const fsPromises = require('fs/promises'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); @@ -14,8 +14,8 @@ function ensureTypstInstalled() { }); } -function writeTextFile(filePath, content) { - fs.writeFileSync(filePath, content, 'utf8'); +async function writeTextFile(filePath, content) { + await fsPromises.writeFile(filePath, content, 'utf8'); } function compileTypst(workingDir, typPath, pdfPath) { @@ -387,29 +387,31 @@ async function generatePdfReport(allDailyData, options = {}) { throw new Error('No data available for the report.'); } - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ttdash-report-')); + const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'ttdash-report-')); const typPath = path.join(tempDir, 'report.typ'); const pdfPath = path.join(tempDir, 'report.pdf'); const jsonPath = path.join(tempDir, 'report.json'); try { - writeTextFile(typPath, buildTemplate()); - writeTextFile(jsonPath, JSON.stringify(reportData, null, 2)); + await writeTextFile(typPath, buildTemplate()); + await writeTextFile(jsonPath, JSON.stringify(reportData, null, 2)); const charts = createChartAssets(reportData); - for (const [filename, content] of Object.entries(charts)) { - writeTextFile(path.join(tempDir, filename), content); - } + await Promise.all( + Object.entries(charts).map(([filename, content]) => + writeTextFile(path.join(tempDir, filename), content), + ), + ); await compileTypst(tempDir, typPath, pdfPath); return { - buffer: fs.readFileSync(pdfPath), + buffer: await fsPromises.readFile(pdfPath), filename: `ttdash-report-${new Date().toISOString().slice(0, 10)}.pdf`, reportData, }; } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); + await fsPromises.rm(tempDir, { recursive: true, force: true }); } } diff --git a/shared/dashboard-domain.d.ts b/shared/dashboard-domain.d.ts index a2a17db..6818e36 100644 --- a/shared/dashboard-domain.d.ts +++ b/shared/dashboard-domain.d.ts @@ -1,19 +1,31 @@ import type { DailyUsage, DashboardMetrics, ViewMode } from './dashboard-types' +/** Aggregates usage rows to the requested dashboard view mode. */ export function aggregateToDailyFormat(data: DailyUsage[], viewMode: ViewMode): DailyUsage[] +/** Returns the busiest rolling seven-day window by cost. */ export function computeBusiestWeek( data: DailyUsage[], ): { start: string; end: string; cost: number } | null +/** Computes the core dashboard metrics for a dataset. */ export function computeMetrics(data: DailyUsage[]): DashboardMetrics +/** Computes a simple moving average over numeric values. */ export function computeMovingAverage( values: Array, window?: number, ): Array +/** Computes the relative week-over-week cost change. */ export function computeWeekOverWeekChange(data: DailyUsage[]): number | null +/** Filters usage rows by an inclusive ISO date range. */ export function filterByDateRange(data: DailyUsage[], start?: string, end?: string): DailyUsage[] +/** Filters usage rows to entries that contain selected models. */ export function filterByModels(data: DailyUsage[], selectedModels: string[]): DailyUsage[] +/** Filters usage rows to a specific calendar month. */ export function filterByMonth(data: DailyUsage[], month: string | null): DailyUsage[] +/** Filters usage rows to entries that contain selected providers. */ export function filterByProviders(data: DailyUsage[], selectedProviders: string[]): DailyUsage[] +/** Resolves the provider name for a model identifier. */ export function getModelProvider(raw: string): string +/** Normalizes raw model names to their dashboard label. */ export function normalizeModelName(raw: string): string +/** Sorts usage rows in ascending date order. */ export function sortByDate(data: DailyUsage[]): DailyUsage[] diff --git a/shared/dashboard-domain.js b/shared/dashboard-domain.js index b097da2..365009c 100644 --- a/shared/dashboard-domain.js +++ b/shared/dashboard-domain.js @@ -132,6 +132,12 @@ function parseOSeries(name) { return `${name.slice(0, separatorIndex)} ${capitalize(name.slice(separatorIndex + 1))}` } +/** + * Normalizes raw model names to their dashboard label. + * + * @param raw - The raw model identifier. + * @returns The normalized display name. + */ function normalizeModelName(raw) { const canonical = canonicalizeModelName(raw) @@ -178,6 +184,12 @@ function normalizeModelName(raw) { return canonical.split('-').filter(Boolean).map(titleCaseSegment).join(' ') || String(raw || '') } +/** + * Resolves the provider name for a model identifier. + * + * @param raw - The raw model identifier. + * @returns The normalized provider name. + */ function getModelProvider(raw) { const canonical = canonicalizeModelName(raw) for (const matcher of PROVIDER_MATCHERS) { @@ -223,6 +235,14 @@ function recalculateDayFromBreakdowns(day, filteredBreakdowns) { } } +/** + * Filters usage rows by an inclusive ISO date range. + * + * @param data - The source usage rows. + * @param start - The optional start date. + * @param end - The optional end date. + * @returns The filtered usage rows. + */ function filterByDateRange(data, start, end) { return data.filter((entry) => { if (start && entry.date < start) return false @@ -231,6 +251,13 @@ function filterByDateRange(data, start, end) { }) } +/** + * Filters usage rows to entries that contain selected models. + * + * @param data - The source usage rows. + * @param selectedModels - The normalized model names to keep. + * @returns The filtered usage rows. + */ function filterByModels(data, selectedModels) { if (!selectedModels || selectedModels.length === 0) return data const selected = new Set(selectedModels) @@ -247,6 +274,13 @@ function filterByModels(data, selectedModels) { .filter(Boolean) } +/** + * Filters usage rows to entries that contain selected providers. + * + * @param data - The source usage rows. + * @param selectedProviders - The provider names to keep. + * @returns The filtered usage rows. + */ function filterByProviders(data, selectedProviders) { if (!selectedProviders || selectedProviders.length === 0) return data const selected = new Set(selectedProviders) @@ -263,15 +297,35 @@ function filterByProviders(data, selectedProviders) { .filter(Boolean) } +/** + * Filters usage rows to a specific calendar month. + * + * @param data - The source usage rows. + * @param month - The month in YYYY-MM format. + * @returns The filtered usage rows. + */ function filterByMonth(data, month) { if (!month) return data return data.filter((entry) => entry.date.startsWith(month)) } +/** + * Sorts usage rows in ascending date order. + * + * @param data - The source usage rows. + * @returns A date-sorted copy of the input. + */ function sortByDate(data) { return [...data].sort((left, right) => left.date.localeCompare(right.date)) } +/** + * Aggregates usage rows to the requested dashboard view mode. + * + * @param data - The source daily usage rows. + * @param viewMode - The target aggregation mode. + * @returns The aggregated usage rows. + */ function aggregateToDailyFormat(data, viewMode) { if (viewMode === 'daily') return data @@ -309,6 +363,13 @@ function aggregateToDailyFormat(data, viewMode) { return Array.from(groups.values()).sort((left, right) => left.date.localeCompare(right.date)) } +/** + * Computes a simple moving average over numeric values. + * + * @param values - The source numeric values. + * @param window - The moving average window size. + * @returns The moving-average series. + */ function computeMovingAverage(values, window = 7) { const result = Array(values.length) let sum = 0 @@ -343,6 +404,12 @@ function stdDev(values) { return Math.sqrt(variance) } +/** + * Returns the busiest rolling seven-day window by cost. + * + * @param data - The source usage rows. + * @returns The top seven-day window or null when unavailable. + */ function computeBusiestWeek(data) { const sorted = data .filter((entry) => /^\d{4}-\d{2}-\d{2}$/.test(entry.date)) @@ -383,6 +450,12 @@ function computeBusiestWeek(data) { return bestWindow } +/** + * Computes the relative week-over-week cost change. + * + * @param data - The source usage rows. + * @returns The relative week-over-week delta. + */ function computeWeekOverWeekChange(data) { if (data.some((entry) => !/^\d{4}-\d{2}-\d{2}$/.test(entry.date))) return null if (data.length < 14) return null @@ -395,6 +468,12 @@ function computeWeekOverWeekChange(data) { return ((lastSum - prevSum) / prevSum) * 100 } +/** + * Computes the core dashboard metrics for a dataset. + * + * @param data - The source usage rows. + * @returns The derived dashboard metrics. + */ function computeMetrics(data) { if (data.length === 0) { return { diff --git a/shared/dashboard-types.d.ts b/shared/dashboard-types.d.ts index 1b0c462..34cc1a7 100644 --- a/shared/dashboard-types.d.ts +++ b/shared/dashboard-types.d.ts @@ -1,3 +1,4 @@ +/** Describes per-model usage totals for one period. */ export interface ModelBreakdown { modelName: string inputTokens: number @@ -9,6 +10,7 @@ export interface ModelBreakdown { requestCount: number } +/** Describes aggregated usage for one daily, monthly, or yearly period. */ export interface DailyUsage { date: string inputTokens: number @@ -24,8 +26,10 @@ export interface DailyUsage { _aggregatedDays?: number } +/** Lists the supported dashboard aggregation modes. */ export type ViewMode = 'daily' | 'monthly' | 'yearly' +/** Collects high-level metrics derived from the current dataset. */ export interface DashboardMetrics { totalCost: number totalTokens: number diff --git a/shared/model-colors.d.ts b/shared/model-colors.d.ts index f4fceff..87a77a2 100644 --- a/shared/model-colors.d.ts +++ b/shared/model-colors.d.ts @@ -1,17 +1,24 @@ +/** Lists the supported shared model color themes. */ export type ModelColorTheme = 'dark' | 'light' +/** Describes the HSL values assigned to one model color. */ export interface ModelColorSpec { h: number s: number l: number } +/** Configures shared model color resolution. */ export interface ModelColorOptions { theme?: ModelColorTheme alpha?: number } +/** Normalizes an unknown theme value to a supported color theme. */ export function normalizeTheme(theme?: string): ModelColorTheme +/** Returns the shared color spec for a normalized model name. */ export function getModelColorSpec(name: string, options?: ModelColorOptions): ModelColorSpec +/** Returns the shared model color as an HSL string. */ export function getModelColor(name: string, options?: ModelColorOptions): string +/** Returns the shared model color as an RGB string. */ export function getModelColorRgb(name: string, options?: ModelColorOptions): string diff --git a/shared/model-colors.js b/shared/model-colors.js index 83da52a..78b1e39 100644 --- a/shared/model-colors.js +++ b/shared/model-colors.js @@ -113,6 +113,12 @@ const MODEL_COLOR_RULES = [ const FALLBACK_HUES = [148, 168, 190, 208, 226, 248, 272, 332, 18, 30, 44] +/** + * Normalizes an unknown theme value to a supported color theme. + * + * @param theme - The requested theme value. + * @returns The normalized shared color theme. + */ function normalizeTheme(theme) { return theme === 'light' ? 'light' : 'dark' } @@ -164,6 +170,13 @@ function fallbackColor(name, theme) { } } +/** + * Returns the shared color spec for a normalized model name. + * + * @param name - The model name to resolve. + * @param options - The theme and alpha options for color resolution. + * @returns The resolved HSL color spec. + */ function getModelColorSpec(name, options = {}) { const theme = normalizeTheme(options.theme) const known = findKnownColor(name) @@ -218,12 +231,26 @@ function hslToRgb(spec) { } } +/** + * Returns the shared model color as an HSL string. + * + * @param name - The model name to resolve. + * @param options - The theme and alpha options for color resolution. + * @returns The resolved CSS color string. + */ function getModelColor(name, options = {}) { const spec = getModelColorSpec(name, options) const alpha = normalizeAlpha(options.alpha) return alpha === null ? toHslString(spec) : toHslaString(spec, alpha) } +/** + * Returns the shared model color as an RGB string. + * + * @param name - The model name to resolve. + * @param options - The theme and alpha options for color resolution. + * @returns The resolved CSS RGB color string. + */ function getModelColorRgb(name, options = {}) { const spec = getModelColorSpec(name, options) const { r, g, b } = hslToRgb(spec) diff --git a/src/App.tsx b/src/App.tsx index d184983..0d5f548 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { MotionConfig } from 'framer-motion' import { TooltipProvider } from '@/components/ui/tooltip' import { ToastProvider } from '@/components/ui/toast' import { Dashboard } from '@/components/Dashboard' @@ -8,11 +9,19 @@ import type { AppSettings } from '@/types' interface AppProps { initialSettings: AppSettings initialSettingsError?: string | null + initialSettingsLoadedFromServer?: boolean + initialSettingsFetchedAt?: number | null } -export function App({ initialSettings, initialSettingsError = null }: AppProps) { +/** Boots the app providers and renders the dashboard shell. */ +export function App({ + initialSettings, + initialSettingsError = null, + initialSettingsLoadedFromServer = false, + initialSettingsFetchedAt = null, +}: AppProps) { const [queryClient] = useState(() => { - const client = new QueryClient({ + return new QueryClient({ defaultOptions: { queries: { retry: 1, @@ -20,17 +29,22 @@ export function App({ initialSettings, initialSettingsError = null }: AppProps) }, }, }) - client.setQueryData(['settings'], initialSettings) - return client }) return ( - - - - - + + + + + + + ) } diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 12b2a4b..e2e8420 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense } from 'react' +import { lazy, Suspense, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { SlidersHorizontal } from 'lucide-react' import { Header } from './layout/Header' @@ -6,12 +6,12 @@ import { FilterBar } from './layout/FilterBar' import { EmptyState } from './EmptyState' import { LoadErrorState } from './LoadErrorState' import { CommandPalette } from './features/command-palette/CommandPalette' -import { SettingsModal } from './features/settings/SettingsModal' import { PDFReportButton } from './features/pdf-report/PDFReport' import { DashboardSections } from './dashboard/DashboardSections' import { DashboardSkeleton } from './ui/skeleton' import { Button } from './ui/button' -import { useDashboardController } from '@/hooks/use-dashboard-controller' +import { useDashboardControllerWithBootstrap } from '@/hooks/use-dashboard-controller' +import type { AppSettings } from '@/types' const DrillDownModal = lazy(() => import('./features/drill-down/DrillDownModal').then((module) => ({ @@ -23,14 +23,38 @@ const AutoImportModal = lazy(() => default: module.AutoImportModal, })), ) +const SettingsModal = lazy(() => + import('./features/settings/SettingsModal').then((module) => ({ + default: module.SettingsModal, + })), +) +const HelpPanel = lazy(() => + import('./features/help/HelpPanel').then((module) => ({ + default: module.HelpPanel, + })), +) interface DashboardProps { + initialSettings: AppSettings initialSettingsError?: string | null + initialSettingsLoadedFromServer?: boolean + initialSettingsFetchedAt?: number | null } -export function Dashboard({ initialSettingsError = null }: DashboardProps) { +/** Renders the full dashboard experience around the shared controller state. */ +export function Dashboard({ + initialSettings, + initialSettingsError = null, + initialSettingsLoadedFromServer = false, + initialSettingsFetchedAt = null, +}: DashboardProps) { const { t } = useTranslation() - const controller = useDashboardController(initialSettingsError) + const controller = useDashboardControllerWithBootstrap( + initialSettings, + initialSettingsLoadedFromServer, + initialSettingsFetchedAt, + initialSettingsError, + ) const { fileInputRef, settingsImportInputRef, @@ -57,7 +81,6 @@ export function Dashboard({ initialSettingsError = null }: DashboardProps) { headerDataSource, startupAutoLoadBadge, animationSeed, - daily, allProviders, settingsProviderOptions, settingsModelOptions, @@ -154,6 +177,32 @@ export function Dashboard({ initialSettingsError = null }: DashboardProps) { ) + const drillDownSequence = useMemo( + () => [...filteredData].sort((a, b) => a.date.localeCompare(b.date)), + [filteredData], + ) + + const drillDownIndex = useMemo( + () => + drillDownDate !== null + ? drillDownSequence.findIndex((entry) => entry.date === drillDownDate) + : -1, + [drillDownDate, drillDownSequence], + ) + + const hasPreviousDrillDown = drillDownIndex > 0 + const hasNextDrillDown = drillDownIndex >= 0 && drillDownIndex < drillDownSequence.length - 1 + + const handleDrillDownPrevious = useCallback(() => { + if (!hasPreviousDrillDown) return + setDrillDownDate(drillDownSequence[drillDownIndex - 1]?.date ?? null) + }, [drillDownIndex, drillDownSequence, hasPreviousDrillDown, setDrillDownDate]) + + const handleDrillDownNext = useCallback(() => { + if (!hasNextDrillDown) return + setDrillDownDate(drillDownSequence[drillDownIndex + 1]?.date ?? null) + }, [drillDownIndex, drillDownSequence, hasNextDrillDown, setDrillDownDate]) + const autoImportDialog = ( {autoImportOpen && ( @@ -167,29 +216,39 @@ export function Dashboard({ initialSettingsError = null }: DashboardProps) { ) const settingsDialog = ( - + + {settingsOpen && ( + + )} + + ) + + const helpDialog = ( + + {helpOpen && } + ) if (!fatalLoadState && (isLoading || settingsLoading)) { @@ -221,6 +280,7 @@ export function Dashboard({ initialSettingsError = null }: DashboardProps) { actions={actions} /> {fileInputs} + {helpDialog} ) } @@ -236,19 +296,22 @@ export function Dashboard({ initialSettingsError = null }: DashboardProps) { {fileInputs} {autoImportDialog} {settingsDialog} + {helpDialog} ) } return ( -
+
{fileInputs} + {autoImportDialog} + {settingsDialog} + {helpDialog}
{t('header.settings')} @@ -300,10 +363,7 @@ export function Dashboard({ initialSettingsError = null }: DashboardProps) { />
-
+
@@ -338,11 +399,16 @@ export function Dashboard({ initialSettingsError = null }: DashboardProps) { day={drillDownDay} contextData={filteredData} open={true} + hasPrevious={hasPreviousDrillDown} + hasNext={hasNextDrillDown} + currentIndex={drillDownIndex >= 0 ? drillDownIndex + 1 : 0} + totalCount={drillDownSequence.length} + onPrevious={handleDrillDownPrevious} + onNext={handleDrillDownNext} onClose={() => setDrillDownDate(null)} /> )} - setHelpOpen(true)} onLanguageChange={handleLanguageChange} /> - - {autoImportDialog} - {settingsDialog}
) } diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx index f4ab4c8..7b18b2a 100644 --- a/src/components/EmptyState.tsx +++ b/src/components/EmptyState.tsx @@ -11,35 +11,36 @@ interface EmptyStateProps { onOpenSettings: () => void } +/** Renders the onboarding state shown when no usage data is loaded. */ export function EmptyState({ onUpload, onAutoImport, onOpenSettings }: EmptyStateProps) { const { t } = useTranslation() return ( -
+
- -
+ +

TTDash

-

v{VERSION}

+

v{VERSION}

-

+

{t('emptyState.description')}

- -

{t('emptyState.or')}

- - diff --git a/src/components/LoadErrorState.tsx b/src/components/LoadErrorState.tsx index e144127..348da48 100644 --- a/src/components/LoadErrorState.tsx +++ b/src/components/LoadErrorState.tsx @@ -17,6 +17,7 @@ interface LoadErrorStateProps { actions: LoadErrorAction[] } +/** Renders the fatal load-error state with recovery actions. */ export function LoadErrorState({ title, description, @@ -25,9 +26,9 @@ export function LoadErrorState({ actions, }: LoadErrorStateProps) { return ( -
+
- +
@@ -40,7 +41,7 @@ export function LoadErrorState({ {details.length > 0 ? (
-
+
{detailLabel}
    diff --git a/src/components/cards/MetricCard.tsx b/src/components/cards/MetricCard.tsx index 5286b28..1b98ee2 100644 --- a/src/components/cards/MetricCard.tsx +++ b/src/components/cards/MetricCard.tsx @@ -13,6 +13,7 @@ interface MetricCardProps { className?: string } +/** Renders one compact KPI card with optional trend and tooltip support. */ export function MetricCard({ label, value, @@ -22,28 +23,33 @@ export function MetricCard({ info, className, }: MetricCardProps) { + const trendClassName = + trend && trend.value > 0 + ? 'bg-rose-500/14 text-rose-700 dark:bg-rose-500/12 dark:text-rose-300' + : 'bg-emerald-500/14 text-emerald-700 dark:bg-emerald-500/12 dark:text-emerald-300' + return (
    - + {label} {info && } {icon && {icon}}
    -
    {value}
    +
    {value}
    - {subtitle && {subtitle}} + {subtitle && {subtitle}} {trend && trend.value !== 0 && ( 0 ? 'text-red-400 bg-red-400/10' : 'text-green-400 bg-green-400/10', + 'inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-[10px] font-semibold', + trendClassName, )} > {trend.value > 0 ? '↑' : '↓'} diff --git a/src/components/cards/MonthMetrics.tsx b/src/components/cards/MonthMetrics.tsx index 441a8e4..6a1e03a 100644 --- a/src/components/cards/MonthMetrics.tsx +++ b/src/components/cards/MonthMetrics.tsx @@ -25,6 +25,7 @@ interface MonthMetricsProps { metrics: DashboardMetrics } +/** Renders KPI cards for the current month slice. */ export function MonthMetrics({ daily, metrics }: MonthMetricsProps) { const { t } = useTranslation() const locale = getCurrentLocale() @@ -144,7 +145,7 @@ export function MonthMetrics({ daily, metrics }: MonthMetricsProps) { info={SECTION_HELP.currentMonth} /> -
    +
    } diff --git a/src/components/cards/PrimaryMetrics.tsx b/src/components/cards/PrimaryMetrics.tsx index c022000..a486f90 100644 --- a/src/components/cards/PrimaryMetrics.tsx +++ b/src/components/cards/PrimaryMetrics.tsx @@ -22,6 +22,7 @@ interface PrimaryMetricsProps { viewMode?: ViewMode } +/** Renders the primary dashboard KPI cards. */ export function PrimaryMetrics({ metrics, totalCalendarDays, @@ -66,7 +67,7 @@ export function PrimaryMetrics({ : null return ( -
    +
    +
    -
    +
    } diff --git a/src/components/charts/ChartCard.tsx b/src/components/charts/ChartCard.tsx index 14fcc6b..e9f3c7d 100644 --- a/src/components/charts/ChartCard.tsx +++ b/src/components/charts/ChartCard.tsx @@ -8,7 +8,7 @@ import { type ReactNode, } from 'react' import { useTranslation } from 'react-i18next' -import { motion, useInView } from 'framer-motion' +import { useInView } from 'framer-motion' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog' import { Maximize2 } from 'lucide-react' @@ -16,6 +16,7 @@ import { InfoButton } from '@/components/features/help/InfoButton' import { cn } from '@/lib/cn' import { buildCsvLine } from '@/lib/csv' import { formatCurrency } from '@/lib/formatters' +import { useShouldReduceMotion } from '@/lib/motion' export { stringifyCsvCell } from '@/lib/csv' @@ -33,6 +34,7 @@ interface ChartCardProps { expandedExtra?: ReactNode } +/** Serializes chart rows to a downloadable CSV string. */ export function buildChartCsv(chartData: Record[]): string { if (chartData.length === 0) return '' @@ -48,49 +50,27 @@ export function buildChartCsv(chartData: Record[]): string { const ChartAnimationContext = createContext(false) +/** Returns whether chart-specific animation should currently run. */ export function useChartAnimationActive() { return useContext(ChartAnimationContext) } +/** Exposes the current chart animation state to a render prop. */ export function ChartAnimationAware({ children }: { children: (active: boolean) => ReactNode }) { - return <>{children(useChartAnimationActive())} + const shouldReduceMotion = useShouldReduceMotion() + const animationActive = useChartAnimationActive() + return <>{children(shouldReduceMotion ? false : animationActive)} } interface ChartRevealProps { children: ReactNode variant?: 'line' | 'bar' | 'radial' - delay?: number - duration?: number } -export function ChartReveal({ - children, - variant = 'line', - delay = 0, - duration = 0.7, -}: ChartRevealProps) { - const active = useChartAnimationActive() - const resolvedDuration = variant === 'radial' ? Math.max(duration, 0.95) : Math.max(duration, 0.9) - - const hidden = - variant === 'bar' - ? { opacity: 0, clipPath: 'inset(100% 0 0 0 round 16px)', y: 10 } - : variant === 'radial' - ? { opacity: 0, scale: 0.82, rotate: -18 } - : { opacity: 0, clipPath: 'inset(0 100% 0 0 round 16px)', x: -8 } - - const visible = - variant === 'bar' - ? { opacity: 1, clipPath: 'inset(0 0 0 0 round 16px)', y: 0 } - : variant === 'radial' - ? { opacity: 1, scale: 1, rotate: 0 } - : { opacity: 1, clipPath: 'inset(0 0 0 0 round 16px)', x: 0 } - +/** Wraps chart content in the shared reveal policy for its chart variant. */ +export function ChartReveal({ children, variant = 'line' }: ChartRevealProps) { return ( - {children} - +
    ) } +/** Renders a chart card with export, expand, and stats affordances. */ export function ChartCard({ title, subtitle, @@ -161,11 +142,11 @@ export function ChartCard({ const header = (
    -
    +
    {title} {info && }
    -
    +
    {summary && {summary}}
    @@ -183,7 +164,7 @@ export function ChartCard({ )}
    {stats && ( -
    -
    -
    +
    +
    +
    {t('dashboard.stats.min')}
    -
    {fmt(stats.min)}
    +
    {fmt(stats.min)}
    -
    -
    +
    +
    {t('dashboard.stats.max')}
    -
    {fmt(stats.max)}
    +
    {fmt(stats.max)}
    -
    -
    +
    +
    {t('dashboard.stats.avg')}
    -
    {fmt(stats.avg)}
    +
    {fmt(stats.avg)}
    -
    -
    +
    +
    {t('dashboard.stats.total')}
    -
    +
    {fmt(stats.total)}
    -
    -
    +
    +
    {t('dashboard.stats.dataPoints')}
    -
    {stats.count}
    +
    {stats.count}
    )}
    -
    +
    {renderChildren(true)} {expandedExtra}
    diff --git a/src/components/charts/ChartLegend.tsx b/src/components/charts/ChartLegend.tsx new file mode 100644 index 0000000..1cdd44e --- /dev/null +++ b/src/components/charts/ChartLegend.tsx @@ -0,0 +1,33 @@ +interface ChartLegendEntry { + id?: string | number + dataKey?: string | number + color?: string + value?: string | number +} + +/** Renders a compact responsive legend for Recharts payload items. */ +export function ChartLegend({ payload }: { payload?: ChartLegendEntry[] }) { + if (!payload?.length) return null + + return ( +
    +
    + {payload.map((entry, index) => { + const color = typeof entry.color === 'string' ? entry.color : 'currentColor' + const label = String(entry.value ?? '') + const key = entry.id ?? entry.dataKey ?? `${label}-${color}-${index}` + + return ( +
    + + {label} +
    + ) + })} +
    +
    + ) +} diff --git a/src/components/charts/CorrelationAnalysis.tsx b/src/components/charts/CorrelationAnalysis.tsx index dc0ee12..8c9fc6c 100644 --- a/src/components/charts/CorrelationAnalysis.tsx +++ b/src/components/charts/CorrelationAnalysis.tsx @@ -1,6 +1,6 @@ -import { useMemo, useRef, useState } from 'react' +import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { motion, useInView } from 'framer-motion' +import { useInView } from 'framer-motion' import { ResponsiveContainer, ScatterChart, @@ -22,6 +22,7 @@ import { formatPercent, formatTokens, } from '@/lib/formatters' +import { useShouldReduceMotion } from '@/lib/motion' import type { DailyUsage } from '@/types' interface CorrelationAnalysisProps { @@ -84,8 +85,8 @@ function ScatterTooltip({ if (!point) return null return ( -
    -

    {formatDate(point.label)}

    +
    +

    {formatDate(point.label)}

    {mode === 'requestCost' ? ( <> @@ -144,7 +145,6 @@ function CorrelationPanel({ xTickFormatter, yAxisName, footer, - delay, }: { title: string subtitle: string @@ -156,26 +156,18 @@ function CorrelationPanel({ xTickFormatter?: (value: number) => string yAxisName: string footer: string - delay: number }) { const panelRef = useRef(null) const panelInView = useInView(panelRef, { once: true, amount: 0.45 }) - const [animatePoints, setAnimatePoints] = useState(false) + const shouldReduceMotion = useShouldReduceMotion() + const animatePoints = shouldReduceMotion ? true : panelInView const chartData = animatePoints ? data : [] return ( - { - if (panelInView) setAnimatePoints(true) - }} - > +
    -
    +
    {title}
    {subtitle}
    @@ -209,7 +201,7 @@ function CorrelationPanel({ fill={color} stroke={color} fillOpacity={0.72} - isAnimationActive={animatePoints} + isAnimationActive={!shouldReduceMotion && animatePoints} animationBegin={animationBegin} animationDuration={CHART_ANIMATION.duration} /> @@ -217,10 +209,11 @@ function CorrelationPanel({
    {footer}
    - +
    ) } +/** Renders scatter-plot based correlation analysis for the current dataset. */ export function CorrelationAnalysis({ data }: CorrelationAnalysisProps) { const { t } = useTranslation() const requestVsCost = useMemo( @@ -291,7 +284,7 @@ export function CorrelationAnalysis({ data }: CorrelationAnalysisProps) { - + formatPercent(value, 0)} yAxisName={t('charts.correlation.costPerRequestAxis')} footer={getCorrelationInterpretation(t, cacheEfficiencyCorrelation, 'cacheEfficiency')} - delay={0.08} /> diff --git a/src/components/charts/CostByModel.tsx b/src/components/charts/CostByModel.tsx index 2a4e994..eb64efb 100644 --- a/src/components/charts/CostByModel.tsx +++ b/src/components/charts/CostByModel.tsx @@ -4,7 +4,7 @@ import { ChartCard, ChartAnimationAware, ChartReveal } from './ChartCard' import { CustomTooltip } from './CustomTooltip' import { CHART_ANIMATION } from './chart-theme' import { getModelColor } from '@/lib/model-utils' -import { formatCurrency } from '@/lib/formatters' +import { formatCurrency, formatPercent } from '@/lib/formatters' import { CHART_HELP } from '@/lib/help-content' interface CostByModelProps { @@ -34,14 +34,29 @@ function CenterLabel({ viewBox, total }: { viewBox?: { cx: number; cy: number }; ) } +/** Renders the per-model cost distribution donut. */ export function CostByModel({ data }: CostByModelProps) { const { t } = useTranslation() const total = data.reduce((sum, d) => sum + d.value, 0) + const sortedSegments = [...data].sort((a, b) => b.value - a.value) + const leadingSegments = sortedSegments.slice(0, 3).map((entry) => ({ + ...entry, + share: total > 0 ? (entry.value / total) * 100 : 0, + })) + const remainingValue = sortedSegments.slice(3).reduce((sum, entry) => sum + entry.value, 0) + const topDriver = leadingSegments[0] ?? null return ( + {topDriver.name} · {formatPercent(topDriver.share, 0)} + + ) : undefined + } info={CHART_HELP.costByModel} chartData={data as unknown as Record[]} valueKey="value" @@ -56,44 +71,81 @@ export function CostByModel({ data }: CostByModelProps) { return ( {(animate) => ( - - - - + + + + + {data.map((entry) => ( + + ))} + + + formatCurrency(v)} />} /> + { + const entry = data.find((d) => d.name === value) + return ( + + {value} ({entry ? formatCurrency(entry.value) : ''}) + + ) + }} + /> + + + + +
    + {leadingSegments.map((entry) => ( +
    - {data.map((entry) => ( - - ))} - - - formatCurrency(v)} />} /> - { - const entry = data.find((d) => d.name === value) - return ( - - {value} ({entry ? formatCurrency(entry.value) : ''}) - - ) - }} - /> - - - +
    + +
    +
    + {entry.name} +
    +
    + {formatPercent(entry.share, 0)} · {formatCurrency(entry.value)} +
    +
    +
    +
    + ))} + {remainingValue > 0 && ( +
    +
    + {t('charts.costByModel.otherModels')} +
    +
    + {formatPercent((remainingValue / total) * 100, 0)} ·{' '} + {formatCurrency(remainingValue)} +
    +
    + )} +
    +
    )} ) diff --git a/src/components/charts/CostByModelOverTime.tsx b/src/components/charts/CostByModelOverTime.tsx index 3cf45fc..55d000b 100644 --- a/src/components/charts/CostByModelOverTime.tsx +++ b/src/components/charts/CostByModelOverTime.tsx @@ -10,18 +10,20 @@ import { } from 'recharts' import { useTranslation } from 'react-i18next' import { ChartCard, ChartAnimationAware, ChartReveal } from './ChartCard' +import { ChartLegend } from './ChartLegend' import { CustomTooltip } from './CustomTooltip' import { CHART_COLORS, CHART_MARGIN, CHART_ANIMATION } from './chart-theme' import { getModelColor } from '@/lib/model-utils' +import type { ModelCostChartPoint } from '@/lib/data-transforms' import { coerceNumber, formatCurrency, formatDateAxis } from '@/lib/formatters' import { CHART_HELP } from '@/lib/help-content' -import type { ChartDataPoint } from '@/types' interface CostByModelOverTimeProps { - data: (ChartDataPoint & Record)[] + data: ModelCostChartPoint[] models: string[] } +/** Renders the per-model cost trend over time. */ export function CostByModelOverTime({ data, models }: CostByModelOverTimeProps) { const { t } = useTranslation() const topModel = @@ -40,10 +42,10 @@ export function CostByModelOverTime({ data, models }: CostByModelOverTimeProps) {(animate) => (
    -
    +
    {t('charts.costByModelOverTime.movingAverageHeading')}
    - + @@ -68,7 +70,7 @@ export function CostByModelOverTime({ data, models }: CostByModelOverTimeProps) content={ formatCurrency(v)} />} cursor={{ stroke: 'hsl(var(--muted))', strokeWidth: 1 }} /> - + } /> {models.map((model, index) => ( formatCurrency(v)} />} cursor={{ stroke: 'hsl(var(--muted))', strokeWidth: 1 }} /> - + } /> {models.map((model, index) => ( (null) diff --git a/src/components/charts/CostOverTime.tsx b/src/components/charts/CostOverTime.tsx index 1947d8f..dd269cc 100644 --- a/src/components/charts/CostOverTime.tsx +++ b/src/components/charts/CostOverTime.tsx @@ -12,6 +12,7 @@ import { Legend, } from 'recharts' import { ChartCard, ChartAnimationAware, ChartReveal } from './ChartCard' +import { ChartLegend } from './ChartLegend' import { CustomTooltip } from './CustomTooltip' import { CHART_COLORS, CHART_MARGIN, CHART_ANIMATION } from './chart-theme' import { coerceNumber, formatCurrency, formatDateAxis } from '@/lib/formatters' @@ -23,6 +24,7 @@ interface CostOverTimeProps { onClickDay?: (date: string) => void } +/** Renders the cost-over-time chart with drilldown support. */ export function CostOverTime({ data, onClickDay }: CostOverTimeProps) { const { t } = useTranslation() const uid = useId().replace(/:/g, '') @@ -109,7 +111,7 @@ export function CostOverTime({ data, onClickDay }: CostOverTimeProps) { content={ formatCurrency(v)} />} cursor={{ fill: 'hsl(var(--muted))', opacity: 0.15 }} /> - + } /> {(animate) => ( - + []} margin={CHART_MARGIN}> diff --git a/src/components/charts/CustomTooltip.tsx b/src/components/charts/CustomTooltip.tsx index 3eb39ac..544859d 100644 --- a/src/components/charts/CustomTooltip.tsx +++ b/src/components/charts/CustomTooltip.tsx @@ -18,6 +18,7 @@ interface CustomTooltipProps { hideZeroValues?: boolean } +/** Renders the shared chart tooltip surface. */ export function CustomTooltip({ active, payload, @@ -70,23 +71,23 @@ export function CustomTooltip({ const deltaLabel = t('customTooltip.delta') return ( -
    -

    {label}

    +
    +

    {label}

    {actualEntries.map((entry, i) => { const pct = showTotal && total > 0 ? (entry.value / total) * 100 : null return (
    {entry.name}: - + {formatter ? formatter(entry.value, entry.name) : entry.value} {pct !== null && ( - + {pct.toFixed(0)}% )} @@ -95,28 +96,28 @@ export function CustomTooltip({ })} {showTotal && ( <> -
    +
    - - {totalLabel}: - + + {totalLabel}: + {formatter ? formatter(total, totalLabel) : total} - 100% + 100%
    )} {maEntries.length > 0 && ( <> -
    +
    {maEntries.map((entry, i) => (
    {entry.name}: - + {formatter ? formatter(entry.value, entry.name) : entry.value}
    @@ -125,15 +126,15 @@ export function CustomTooltip({ )} {pinnedEntries.length > 0 && ( <> -
    +
    {pinnedEntries.map((entry, i) => (
    {entry.name}: - + {formatter ? formatter(entry.value, entry.name) : entry.value}
    @@ -142,12 +143,12 @@ export function CustomTooltip({ )} {(deltaVsPrevious !== null || deltaVsAverage !== null) && ( <> -
    +
    {deltaVsPrevious !== null && (
    - + {t('customTooltip.vsPrevious')}: - + {deltaVsPrevious >= 0 ? '+' : ''} {formatter ? formatter(deltaVsPrevious, deltaLabel) : deltaVsPrevious} @@ -155,9 +156,9 @@ export function CustomTooltip({ )} {deltaVsAverage !== null && (
    - + {t('customTooltip.vsAverage')}: - + {deltaVsAverage >= 0 ? '+' : ''} {formatter ? formatter(deltaVsAverage, deltaLabel) : deltaVsAverage} diff --git a/src/components/charts/DistributionAnalysis.tsx b/src/components/charts/DistributionAnalysis.tsx index d3ef43b..5522a02 100644 --- a/src/components/charts/DistributionAnalysis.tsx +++ b/src/components/charts/DistributionAnalysis.tsx @@ -1,6 +1,5 @@ import { useId, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { motion } from 'framer-motion' import { ResponsiveContainer, BarChart, @@ -16,6 +15,7 @@ import { InfoHeading } from '@/components/features/help/InfoHeading' import { CHART_COLORS, CHART_MARGIN, CHART_ANIMATION } from './chart-theme' import { CHART_HELP } from '@/lib/help-content' import { formatCurrency, formatNumber, formatTokens, periodLabel } from '@/lib/formatters' +import { useShouldReduceMotion } from '@/lib/motion' import type { DailyUsage, ViewMode } from '@/types' interface DistributionAnalysisProps { @@ -74,8 +74,8 @@ function DistributionTooltip({ if (!entry) return null return ( -
    -

    {entry.payload.label}

    +
    +

    {entry.payload.label}

    {t('charts.distribution.interval')} @@ -90,9 +90,11 @@ function DistributionTooltip({ ) } +/** Renders histogram-based distribution analysis for cost and request metrics. */ export function DistributionAnalysis({ data, viewMode = 'daily' }: DistributionAnalysisProps) { const { t } = useTranslation() const uid = useId().replace(/:/g, '') + const shouldReduceMotion = useShouldReduceMotion() const distributions = useMemo(() => { if (data.length < 2) return [] @@ -149,16 +151,10 @@ export function DistributionAnalysis({ data, viewMode = 'daily' }: DistributionA {distributions.map((distribution, index) => ( - +
    -
    +
    {distribution.title}
    @@ -199,7 +195,7 @@ export function DistributionAnalysis({ data, viewMode = 'daily' }: DistributionA dataKey="count" radius={[6, 6, 0, 0]} fill={`url(#${uid}-distribution-${index})`} - isAnimationActive + isAnimationActive={!shouldReduceMotion} animationBegin={CHART_ANIMATION.stagger * index} animationDuration={CHART_ANIMATION.duration} > @@ -218,7 +214,7 @@ export function DistributionAnalysis({ data, viewMode = 'daily' }: DistributionA
    - +
    ))} diff --git a/src/components/charts/ModelMix.tsx b/src/components/charts/ModelMix.tsx index d08a483..6c1a216 100644 --- a/src/components/charts/ModelMix.tsx +++ b/src/components/charts/ModelMix.tsx @@ -30,17 +30,17 @@ function MixTooltip({ active, payload, label }: MixTooltipProps) { if (!active || !payload?.length) return null const sorted = [...payload].sort((a, b) => b.value - a.value) return ( -
    -

    {label}

    +
    +

    {label}

    {sorted.map((entry, i) => (
    {entry.name}: - + {formatPercent(entry.value)}
    @@ -50,6 +50,7 @@ function MixTooltip({ active, payload, label }: MixTooltipProps) { ) } +/** Renders the stacked model-mix chart for cost share over time. */ export function ModelMix({ data }: ModelMixProps) { const { t } = useTranslation() const { chartData, models } = useMemo(() => { @@ -87,7 +88,7 @@ export function ModelMix({ data }: ModelMixProps) { > {(animate) => ( - + diff --git a/src/components/charts/RequestCacheHitRateByModel.tsx b/src/components/charts/RequestCacheHitRateByModel.tsx index ff999e8..36806a7 100644 --- a/src/components/charts/RequestCacheHitRateByModel.tsx +++ b/src/components/charts/RequestCacheHitRateByModel.tsx @@ -15,6 +15,7 @@ import { Cell, } from 'recharts' import { ChartAnimationAware, ChartCard, ChartReveal } from './ChartCard' +import { ChartLegend } from './ChartLegend' import { CHART_ANIMATION, CHART_COLORS, CHART_MARGIN } from './chart-theme' import { CustomTooltip } from './CustomTooltip' import { CHART_HELP } from '@/lib/help-content' @@ -44,6 +45,7 @@ function computePointRate( return base > 0 ? (cacheRead / base) * 100 : 0 } +/** Renders cache hit-rate comparisons grouped by model. */ export function RequestCacheHitRateByModel({ timelineData, summaryData, @@ -172,13 +174,13 @@ export function RequestCacheHitRateByModel({ ) const expandedExtra = ( -
    +
    {barData.slice(0, 6).map((entry) => (
    {entry.model}
    -
    +
    {t('charts.requestCacheHitRate.totalRate')}
    @@ -186,7 +188,7 @@ export function RequestCacheHitRateByModel({
    -
    +
    {t('charts.requestCacheHitRate.trailing7Rate')}
    @@ -214,9 +216,9 @@ export function RequestCacheHitRateByModel({ > {(expanded) => ( <> -
    +
    -
    +
    {t('charts.requestCacheHitRate.totalRate')}
    @@ -224,7 +226,7 @@ export function RequestCacheHitRateByModel({
    -
    +
    {t('charts.requestCacheHitRate.trailing7Rate')}
    @@ -232,13 +234,13 @@ export function RequestCacheHitRateByModel({
    -
    +
    {t('charts.requestCacheHitRate.topModel')}
    -
    {summary.topModel?.model ?? '–'}
    +
    {summary.topModel?.model ?? '–'}
    -
    +
    {t('charts.requestCacheHitRate.models')}
    {summary.models}
    @@ -249,12 +251,12 @@ export function RequestCacheHitRateByModel({ className={`grid gap-4 ${expanded ? 'grid-cols-1 xl:grid-cols-[2fr_1fr]' : 'grid-cols-1 lg:grid-cols-[2fr_1fr]'}`} >
    -
    +
    {t('charts.requestCacheHitRate.timelineHeading', { unit: periodUnit(viewMode) })}
    {(animate) => ( - + - + } />
    -
    +
    {t('charts.requestCacheHitRate.modelBreakdownHeading')}
    {(animate) => ( - + - + } /> (
    -
    +
    {trendHeading}
    - + @@ -160,7 +162,7 @@ export function RequestsOverTime({ data, viewMode = 'daily', onClickDay }: Reque content={ formatRequests(v)} />} cursor={{ stroke: 'hsl(var(--muted))', strokeWidth: 1 }} /> - + } />
    -
    +
    {t('charts.requestsOverTime.requestsByModelTotal')}
    -
    +
    {(summary?.topModels ?? []).map(([model, total]) => { const share = summary && summary.totalRequests > 0 ? (total / summary.totalRequests) * 100 : 0 return (
    -
    +
    {model}
    @@ -259,9 +261,9 @@ export function RequestsOverTime({ data, viewMode = 'daily', onClickDay }: Reque return ( <> -
    +
    -
    +
    {t('charts.requestsOverTime.total')}
    @@ -269,7 +271,7 @@ export function RequestsOverTime({ data, viewMode = 'daily', onClickDay }: Reque
    -
    +
    {averageLabel}
    @@ -279,15 +281,15 @@ export function RequestsOverTime({ data, viewMode = 'daily', onClickDay }: Reque
    -
    +
    {t('charts.requestsOverTime.topModel')}
    -
    +
    {summary?.topModels[0]?.[0] ?? '–'}
    -
    +
    {t('charts.requestsOverTime.topShare')}
    @@ -352,7 +354,7 @@ export function RequestsOverTime({ data, viewMode = 'daily', onClickDay }: Reque } cursor={{ fill: 'hsl(var(--muted))', opacity: 0.12 }} /> - + } /> {(animate) => ( - + {(animate) => ( - + diff --git a/src/components/charts/TokenTypes.tsx b/src/components/charts/TokenTypes.tsx index 5c599a0..2b7d89d 100644 --- a/src/components/charts/TokenTypes.tsx +++ b/src/components/charts/TokenTypes.tsx @@ -41,6 +41,7 @@ function CenterLabel({ viewBox, total }: { viewBox?: { cx: number; cy: number }; ) } +/** Renders the token composition donut chart. */ export function TokenTypes({ data }: TokenTypesProps) { const { t } = useTranslation() const total = data.reduce((sum, d) => sum + d.value, 0) @@ -63,7 +64,7 @@ export function TokenTypes({ data }: TokenTypesProps) { return ( {(animate) => ( - + void } +/** Renders token volume over time with drilldown support. */ export function TokensOverTime({ data, onClickDay }: TokensOverTimeProps) { const { t } = useTranslation() const uid = useId() @@ -83,10 +84,10 @@ export function TokensOverTime({ data, onClickDay }: TokensOverTimeProps) { {(animate) => (
    -
    +
    {t('charts.tokensOverTime.allTypes')}
    - + @@ -159,7 +160,7 @@ export function TokensOverTime({ data, onClickDay }: TokensOverTimeProps) { expandedExtra={totalChart} > {/* Summary row with totals per type */} -
    +
    {( [ { label: 'Cache Read', value: totals.cacheRead, color: CHART_COLORS.cacheRead }, @@ -170,13 +171,13 @@ export function TokensOverTime({ data, onClickDay }: TokensOverTimeProps) { ] as const ).map((item) => (
    -
    +
    {item.label}
    -
    +
    {formatTokens(item.value)}
    -
    +
    {totals.total > 0 ? `${((item.value / totals.total) * 100).toFixed(1)}%` : '–'}
    @@ -185,7 +186,7 @@ export function TokensOverTime({ data, onClickDay }: TokensOverTimeProps) { {/* Chart 1: Cache Tokens (large scale) with per-type MA7 */}
    -
    +
    {t('charts.tokensOverTime.cacheTokens')}
    @@ -283,12 +284,12 @@ export function TokensOverTime({ data, onClickDay }: TokensOverTimeProps) { {/* Chart 2: I/O Tokens (small scale) with per-type MA7 */}
    -
    +
    {t('charts.tokensOverTime.inputOutputTokens')}
    {(animate) => ( - + @@ -375,12 +376,12 @@ export function TokensOverTime({ data, onClickDay }: TokensOverTimeProps) {
    -
    +
    {t('charts.tokensOverTime.thinkingTokens')}
    {(animate) => ( - + diff --git a/src/components/charts/chart-theme.ts b/src/components/charts/chart-theme.ts index 8e067ac..f17ccb1 100644 --- a/src/components/charts/chart-theme.ts +++ b/src/components/charts/chart-theme.ts @@ -1,19 +1,22 @@ +/** Defines shared chart colors derived from the active theme tokens. */ export const CHART_COLORS = { - cost: 'hsl(215, 70%, 55%)', - ma7: 'hsl(262, 60%, 65%)', - cumulative: 'hsl(160, 50%, 52%)', - input: 'hsl(340, 55%, 52%)', - output: 'hsl(35, 80%, 52%)', - cacheWrite: 'hsl(262, 60%, 55%)', - cacheRead: 'hsl(160, 50%, 42%)', - grid: 'hsl(224, 12%, 18%)', - axis: 'hsl(220, 8%, 46%)', - tooltipBg: 'hsl(224, 18%, 10%)', - tooltipBorder: 'hsl(224, 12%, 18%)', + cost: 'hsl(var(--chart-1))', + ma7: 'hsl(var(--chart-2))', + cumulative: 'hsl(var(--chart-3))', + input: 'hsl(var(--chart-5))', + output: 'hsl(var(--chart-4))', + cacheWrite: 'hsl(var(--chart-2))', + cacheRead: 'hsl(var(--chart-3))', + grid: 'hsl(var(--border))', + axis: 'hsl(var(--muted-foreground))', + tooltipBg: 'hsl(var(--popover))', + tooltipBorder: 'hsl(var(--border))', } +/** Defines the default chart margins used across the dashboard. */ export const CHART_MARGIN = { top: 5, right: 10, left: 10, bottom: 5 } +/** Defines the shared chart animation timings. */ export const CHART_ANIMATION = { duration: 800, easing: 'ease-out' as const, @@ -21,7 +24,7 @@ export const CHART_ANIMATION = { slowDuration: 1200, } -/** Generate a CSS-safe gradient ID from a name */ +/** Generates a CSS-safe gradient id from an arbitrary name. */ export function gradientId(name: string): string { return `grad-${name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}` } diff --git a/src/components/dashboard/DashboardSections.tsx b/src/components/dashboard/DashboardSections.tsx index 0ca5729..009a728 100644 --- a/src/components/dashboard/DashboardSections.tsx +++ b/src/components/dashboard/DashboardSections.tsx @@ -1,4 +1,4 @@ -import { Fragment } from 'react' +import { Fragment, Suspense, lazy } from 'react' import { useTranslation } from 'react-i18next' import { PrimaryMetrics } from '../cards/PrimaryMetrics' import { SecondaryMetrics } from '../cards/SecondaryMetrics' @@ -6,33 +6,17 @@ import { TodayMetrics } from '../cards/TodayMetrics' import { MonthMetrics } from '../cards/MonthMetrics' import { CostOverTime } from '../charts/CostOverTime' import { CostByModel } from '../charts/CostByModel' -import { CostByModelOverTime } from '../charts/CostByModelOverTime' -import { CumulativeCost } from '../charts/CumulativeCost' -import { TokensOverTime } from '../charts/TokensOverTime' -import { RequestsOverTime } from '../charts/RequestsOverTime' -import { RequestCacheHitRateByModel } from '../charts/RequestCacheHitRateByModel' -import { TokenTypes } from '../charts/TokenTypes' -import { CostByWeekday } from '../charts/CostByWeekday' -import { TokenEfficiency } from '../charts/TokenEfficiency' -import { ModelMix } from '../charts/ModelMix' -import { DistributionAnalysis } from '../charts/DistributionAnalysis' -import { CorrelationAnalysis } from '../charts/CorrelationAnalysis' -import { ModelEfficiency } from '../tables/ModelEfficiency' -import { ProviderEfficiency } from '../tables/ProviderEfficiency' -import { RecentDays } from '../tables/RecentDays' import { HeatmapCalendar } from '../features/heatmap/HeatmapCalendar' -import { CostForecast } from '../features/forecast/CostForecast' -import { CacheROI } from '../features/cache-roi/CacheROI' -import { PeriodComparison } from '../features/comparison/PeriodComparison' -import { AnomalyDetection } from '../features/anomaly/AnomalyDetection' import { UsageInsights } from '../features/insights/UsageInsights' import { ConcentrationRisk } from '../features/risk/ConcentrationRisk' -import { RequestQuality } from '../features/request-quality/RequestQuality' import { FadeIn } from '../features/animations/FadeIn' -import { ProviderLimitsSection } from '../features/limits/ProviderLimitsSection' import { SectionHeader } from '../ui/section-header' import { ExpandableCard } from '../ui/expandable-card' +import { ChartCardSkeleton } from '../ui/skeleton' +import { ErrorBoundary } from '../ui/error-boundary' import { SECTION_HELP } from '@/lib/help-content' +import { cn } from '@/lib/cn' +import type { ModelCostChartPoint } from '@/lib/data-transforms' import { formatCurrency, formatPercent, formatTokens, periodUnit } from '@/lib/formatters' import type { AggregateMetrics, @@ -47,6 +31,107 @@ import type { WeekdayData, } from '@/types' +const CostForecast = lazy(() => + import('../features/forecast/CostForecast').then((module) => ({ + default: module.CostForecast, + })), +) +const CostByModelOverTime = lazy(() => + import('../charts/CostByModelOverTime').then((module) => ({ + default: module.CostByModelOverTime, + })), +) +const CumulativeCost = lazy(() => + import('../charts/CumulativeCost').then((module) => ({ + default: module.CumulativeCost, + })), +) +const CostByWeekday = lazy(() => + import('../charts/CostByWeekday').then((module) => ({ + default: module.CostByWeekday, + })), +) +const TokenEfficiency = lazy(() => + import('../charts/TokenEfficiency').then((module) => ({ + default: module.TokenEfficiency, + })), +) +const ModelMix = lazy(() => + import('../charts/ModelMix').then((module) => ({ + default: module.ModelMix, + })), +) +const TokensOverTime = lazy(() => + import('../charts/TokensOverTime').then((module) => ({ + default: module.TokensOverTime, + })), +) +const TokenTypes = lazy(() => + import('../charts/TokenTypes').then((module) => ({ + default: module.TokenTypes, + })), +) +const RequestsOverTime = lazy(() => + import('../charts/RequestsOverTime').then((module) => ({ + default: module.RequestsOverTime, + })), +) +const RequestCacheHitRateByModel = lazy(() => + import('../charts/RequestCacheHitRateByModel').then((module) => ({ + default: module.RequestCacheHitRateByModel, + })), +) +const CacheROI = lazy(() => + import('../features/cache-roi/CacheROI').then((module) => ({ + default: module.CacheROI, + })), +) +const ProviderLimitsSection = lazy(() => + import('../features/limits/ProviderLimitsSection').then((module) => ({ + default: module.ProviderLimitsSection, + })), +) +const RequestQuality = lazy(() => + import('../features/request-quality/RequestQuality').then((module) => ({ + default: module.RequestQuality, + })), +) +const DistributionAnalysis = lazy(() => + import('../charts/DistributionAnalysis').then((module) => ({ + default: module.DistributionAnalysis, + })), +) +const CorrelationAnalysis = lazy(() => + import('../charts/CorrelationAnalysis').then((module) => ({ + default: module.CorrelationAnalysis, + })), +) +const PeriodComparison = lazy(() => + import('../features/comparison/PeriodComparison').then((module) => ({ + default: module.PeriodComparison, + })), +) +const AnomalyDetection = lazy(() => + import('../features/anomaly/AnomalyDetection').then((module) => ({ + default: module.AnomalyDetection, + })), +) +const ModelEfficiency = lazy(() => + import('../tables/ModelEfficiency').then((module) => ({ + default: module.ModelEfficiency, + })), +) +const ProviderEfficiency = lazy(() => + import('../tables/ProviderEfficiency').then((module) => ({ + default: module.ProviderEfficiency, + })), +) +const RecentDays = lazy(() => + import('../tables/RecentDays').then((module) => ({ + default: module.RecentDays, + })), +) + interface DashboardSectionsProps { sectionOrder: DashboardSectionId[] sectionVisibility: Record @@ -63,7 +148,7 @@ interface DashboardSectionsProps { allModels: string[] costChartData: ChartDataPoint[] modelPieData: Array<{ name: string; value: number }> - modelCostChartData: Array> + modelCostChartData: ModelCostChartPoint[] weekdayData: WeekdayData[] tokenChartData: TokenChartDataPoint[] tokenPieData: Array<{ name: string; value: number }> @@ -84,9 +169,11 @@ interface DashboardSectionsProps { } > providerMetrics: Map + isDark: boolean onDrillDownDateChange: (date: string | null) => void } +/** Renders the ordered dashboard sections for the active filters and settings. */ export function DashboardSections({ sectionOrder, sectionVisibility, @@ -111,10 +198,39 @@ export function DashboardSections({ comparisonData, modelCosts, providerMetrics, + isDark, onDrillDownDateChange, }: DashboardSectionsProps) { const { t } = useTranslation() + const lazyCardFallback = (className?: string) => ( + + ) + + const lazyErrorFallback = (className?: string) => ( +
    +

    {t('dashboard.lazySectionError.title')}

    +

    + {t('dashboard.lazySectionError.description')} +

    +
    + ) + + const renderLazySection = (content: React.ReactNode, className?: string) => ( + + {content} + + ) + const renderSection = (sectionId: DashboardSectionId) => { switch (sectionId) { case 'insights': @@ -143,7 +259,7 @@ export function DashboardSections({ viewMode={viewMode} /> - +
    - +
    - - - + + +
    @@ -197,30 +328,36 @@ export function DashboardSections({ description={t('dashboard.forecastCache.description')} info={SECTION_HELP.forecastCache} /> - +
    - - - - - - + {renderLazySection( + + + , + 'h-[360px]', + )} + {renderLazySection( + + + , + 'h-[360px]', + )}
    @@ -228,13 +365,16 @@ export function DashboardSections({ case 'limits': return sectionVisibility.limits ? (
    - - + + {renderLazySection( + , + 'h-[420px]', + )}
    ) : null @@ -247,7 +387,7 @@ export function DashboardSections({ description={t('dashboard.costAnalysis.description')} info={SECTION_HELP.costAnalysis} /> - +
    @@ -255,21 +395,27 @@ export function DashboardSections({
    - +
    - + {renderLazySection( + , + 'h-[320px]', + )}
    - +
    - - + {renderLazySection( + , + 'h-[320px]', + )} + {renderLazySection(, 'h-[320px]')}
    - +
    - - + {renderLazySection(, 'h-[320px]')} + {renderLazySection(, 'h-[320px]')}
    @@ -282,10 +428,13 @@ export function DashboardSections({ description={t('dashboard.tokenAnalysis.description')} info={SECTION_HELP.tokenAnalysis} /> - +
    - - + {renderLazySection( + , + 'h-[320px]', + )} + {renderLazySection(, 'h-[320px]')}
    @@ -298,25 +447,34 @@ export function DashboardSections({ description={t('dashboard.requestAnalysis.description')} info={SECTION_HELP.requestAnalysis} /> - - + + {renderLazySection( + , + 'h-[320px]', + )} - +
    - + {renderLazySection( + , + 'h-[320px]', + )}
    - +
    - + {renderLazySection( + , + 'h-[280px]', + )}
    @@ -329,9 +487,12 @@ export function DashboardSections({ description={t('dashboard.advancedAnalysis.description')} info={SECTION_HELP.advancedAnalysis} /> - +
    - + {renderLazySection( + , + 'h-[320px]', + )}
    - +
    - + {renderLazySection(, 'h-[320px]')}
    @@ -355,36 +516,48 @@ export function DashboardSections({ description={t('dashboard.comparisons.description')} info={SECTION_HELP.comparisons} /> - +
    - - - - - - + {renderLazySection( + + + , + 'h-[360px]', + )} + {renderLazySection( + + + , + 'h-[360px]', + )}
    @@ -397,29 +570,38 @@ export function DashboardSections({ description={t('dashboard.tables.description')} info={SECTION_HELP.tables} /> - - - - -
    - + {renderLazySection( + + />, + 'h-[320px]', + )} + + +
    + {renderLazySection( + , + 'h-[320px]', + )}
    - +
    - + {renderLazySection( + , + 'h-[360px]', + )}
    diff --git a/src/components/features/animations/CountUp.tsx b/src/components/features/animations/CountUp.tsx index 2aed647..786b347 100644 --- a/src/components/features/animations/CountUp.tsx +++ b/src/components/features/animations/CountUp.tsx @@ -7,6 +7,7 @@ interface CountUpProps { className?: string } +/** Animates a numeric value from zero to its final display value. */ export function CountUp({ end, duration = 800, formatter, className }: CountUpProps) { const [current, setCurrent] = useState(0) const startTime = useRef(null) diff --git a/src/components/features/animations/FadeIn.tsx b/src/components/features/animations/FadeIn.tsx index 29cc150..8d05afe 100644 --- a/src/components/features/animations/FadeIn.tsx +++ b/src/components/features/animations/FadeIn.tsx @@ -1,5 +1,6 @@ import { motion } from 'framer-motion' import type { ReactNode } from 'react' +import { useShouldReduceMotion } from '@/lib/motion' interface FadeInProps { children: ReactNode @@ -9,6 +10,7 @@ interface FadeInProps { direction?: 'up' | 'down' | 'left' | 'right' | 'none' } +/** Reveals content when it enters the viewport. */ export function FadeIn({ children, delay = 0, @@ -16,6 +18,7 @@ export function FadeIn({ className, direction = 'up', }: FadeInProps) { + const shouldReduceMotion = useShouldReduceMotion() const offsets = { up: { y: 20 }, down: { y: -20 }, @@ -24,6 +27,10 @@ export function FadeIn({ none: {}, } + if (shouldReduceMotion) { + return
    {children}
    + } + return ( { @@ -38,7 +38,7 @@ export function AnomalyDetection({ data, onClickDay, viewMode = 'daily' }: Anoma

    {t('anomaly.none')}

    -

    {t('anomaly.withinStdDev')}

    +

    {t('anomaly.withinStdDev')}

    @@ -49,14 +49,14 @@ export function AnomalyDetection({ data, onClickDay, viewMode = 'daily' }: Anoma - + {t('anomaly.title', { period: periodLabel(viewMode, true) })} ({anomalies.length}) -

    +

    {t('anomaly.description', { period: periodLabel(viewMode, true), mean: formatCurrency(mean), @@ -71,37 +71,41 @@ export function AnomalyDetection({ data, onClickDay, viewMode = 'daily' }: Anoma const zScore = zScoreNum.toFixed(1) const isHigh = day.totalCost > mean const severity = Math.abs(zScoreNum) >= 3 ? 'critical' : 'warn' + const cardClasses = `flex w-full items-center justify-between rounded-lg p-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${ + severity === 'critical' + ? 'bg-red-400/10 hover:bg-red-400/20 border border-red-400/20' + : 'bg-muted/30 hover:bg-muted/50' + }` return ( -

    onClickDay?.(day.date)} >
    {formatDate(day.date, 'long')} {severity === 'critical' && ( - + {t('anomaly.critical')} )}
    {formatCurrency(day.totalCost)} @@ -109,7 +113,7 @@ export function AnomalyDetection({ data, onClickDay, viewMode = 'daily' }: Anoma {zScore}σ
    -
    + ) })}
    diff --git a/src/components/features/auto-import/AutoImportModal.tsx b/src/components/features/auto-import/AutoImportModal.tsx index 654194c..2dea986 100644 --- a/src/components/features/auto-import/AutoImportModal.tsx +++ b/src/components/features/auto-import/AutoImportModal.tsx @@ -34,6 +34,7 @@ const lineColors: Record = { error: 'text-destructive', } +/** Renders the guided auto-import modal and progress UI. */ export function AutoImportModal({ open, onOpenChange, onSuccess }: AutoImportModalProps) { const { t } = useTranslation() const [status, setStatus] = useState('idle') @@ -143,7 +144,7 @@ export function AutoImportModal({ open, onOpenChange, onSuccess }: AutoImportMod {/* Terminal output */}
    {lines.length === 0 && ( {t('autoImportModal.connecting')} diff --git a/src/components/features/cache-roi/CacheROI.tsx b/src/components/features/cache-roi/CacheROI.tsx index f57bfea..7d1939a 100644 --- a/src/components/features/cache-roi/CacheROI.tsx +++ b/src/components/features/cache-roi/CacheROI.tsx @@ -1,14 +1,13 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' -import { formatPercent } from '@/lib/formatters' +import { formatPercent, periodUnit } from '@/lib/formatters' import { normalizeModelName } from '@/lib/model-utils' import { MODEL_PRICES } from '@/lib/constants' import { Zap } from 'lucide-react' import { FormattedValue } from '@/components/ui/formatted-value' import { InfoHeading } from '@/components/features/help/InfoHeading' import { CHART_HELP } from '@/lib/help-content' -import { periodUnit } from '@/lib/formatters' import type { DailyUsage, ViewMode } from '@/types' interface CacheROIProps { @@ -16,6 +15,7 @@ interface CacheROIProps { viewMode?: ViewMode } +/** Renders the cache savings versus no-cache cost comparison. */ export function CacheROI({ data, viewMode = 'daily' }: CacheROIProps) { const { t } = useTranslation() const { actualCost, hypotheticalCost, savings, savingsPercent, dailyAvg, heuristicModels } = @@ -62,25 +62,40 @@ export function CacheROI({ data, viewMode = 'daily' }: CacheROIProps) { return ( - + {t('cacheRoi.title')} -

    {t('cacheRoi.noData')}

    +

    {t('cacheRoi.noData')}

    ) } - const barWidth = hypotheticalCost > 0 ? (actualCost / hypotheticalCost) * 100 : 100 + const savingsSign = Math.sign(savings) + const hasPositiveSavings = savingsSign > 0 + const barWidth = Math.max( + 0, + Math.min(100, hypotheticalCost > 0 ? (actualCost / hypotheticalCost) * 100 : 100), + ) + const withoutCacheTextClass = 'text-rose-700 dark:text-rose-300' + const withCacheTextClass = + savingsSign < 0 ? 'text-rose-700 dark:text-rose-300' : 'text-emerald-700 dark:text-emerald-300' + const barTrackDangerClass = 'bg-rose-500/12 dark:bg-rose-500/18' + const barFillDangerClass = 'bg-rose-500/60 dark:bg-rose-400/60' + const barFillSuccessClass = 'bg-emerald-500/65 dark:bg-emerald-400/60' + const barSavedSegmentClass = + 'bg-emerald-500/12 dark:bg-emerald-400/16 border-l border-emerald-500/35 dark:border-emerald-400/30 border-dashed' + const barSavedSwatchClass = + 'bg-emerald-500/12 dark:bg-emerald-400/16 border border-emerald-500/35 dark:border-emerald-400/30 border-dashed' return ( - + {t('cacheRoi.title')} @@ -88,7 +103,7 @@ export function CacheROI({ data, viewMode = 'daily' }: CacheROIProps) { {heuristicModels.length > 0 && ( -
    +
    {t('cacheRoi.heuristicFallback', { count: heuristicModels.length, modelsLabel: @@ -96,24 +111,26 @@ export function CacheROI({ data, viewMode = 'daily' }: CacheROIProps) { })}
    )} -
    +
    {t('cacheRoi.withoutCache')}
    -
    +
    {t('cacheRoi.withCacheActual')}
    -
    +
    {t('cacheRoi.savings')}
    -
    +
    - ({formatPercent(savingsPercent)}) + + ({formatPercent(savingsPercent)}) +
    @@ -129,28 +146,32 @@ export function CacheROI({ data, viewMode = 'daily' }: CacheROIProps) { {/* Visual bar comparison */}
    - {t('cacheRoi.withoutCache')} -
    -
    + {t('cacheRoi.withoutCache')} +
    +
    - {t('cacheRoi.withCache')} -
    + {t('cacheRoi.withCache')} +
    -
    +
    - {t('cacheRoi.paid')} + {t('cacheRoi.paid')} - {' '} - {t('cacheRoi.saved')} + {t('cacheRoi.saved')}
    diff --git a/src/components/features/command-palette/CommandPalette.tsx b/src/components/features/command-palette/CommandPalette.tsx index e21a3dd..9b56931 100644 --- a/src/components/features/command-palette/CommandPalette.tsx +++ b/src/components/features/command-palette/CommandPalette.tsx @@ -181,6 +181,7 @@ function getCommandSearchScore(cmd: CommandItem, query: string) { return score } +/** Renders the keyboard-first command palette for dashboard actions. */ export function CommandPalette({ isDark, availableProviders, @@ -653,12 +654,12 @@ export function CommandPalette({ return ( - + {t('commandPalette.title')} {t('commandPalette.description')}
    - + runCommand(cmd)} - className="flex items-center gap-2 rounded-md px-2 py-2 text-sm cursor-pointer aria-selected:bg-accent" + className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm aria-selected:bg-accent" > {cmd.icon} -
    +
    {cmd.label}
    {cmd.description && ( -
    +
    {cmd.description}
    )}
    {cmd.shortcut && ( - + {cmd.shortcut} )} {quickIndex >= 0 && ( - + {quickIndex + 1} )} diff --git a/src/components/features/comparison/PeriodComparison.tsx b/src/components/features/comparison/PeriodComparison.tsx index 399d2c7..0792f71 100644 --- a/src/components/features/comparison/PeriodComparison.tsx +++ b/src/components/features/comparison/PeriodComparison.tsx @@ -36,6 +36,7 @@ function getDelta( return { value: Math.abs(pct), color, arrow, hasData: true } } +/** Renders KPI deltas between the current and previous period. */ export function PeriodComparison({ data }: PeriodComparisonProps) { const { t } = useTranslation() const [preset, setPreset] = useState('week') @@ -114,7 +115,7 @@ export function PeriodComparison({ data }: PeriodComparisonProps) {

    {t('comparison.notEnoughData')}

    -

    +

    {t('comparison.requiresDays', { count: data.length })}

    @@ -179,7 +180,7 @@ export function PeriodComparison({ data }: PeriodComparisonProps) { variant={preset === 'week' ? 'default' : 'outline'} size="sm" onClick={() => setPreset('week')} - className="text-xs h-7" + className="h-7 text-xs" > {t('comparison.week')} @@ -187,7 +188,7 @@ export function PeriodComparison({ data }: PeriodComparisonProps) { variant={preset === 'month' ? 'default' : 'outline'} size="sm" onClick={() => setPreset('month')} - className="text-xs h-7" + className="h-7 text-xs" > {t('comparison.month')} @@ -203,7 +204,7 @@ export function PeriodComparison({ data }: PeriodComparisonProps) { {t('comparison.metric')} {labelB} - + {labelA} {t('comparison.delta')} @@ -216,13 +217,13 @@ export function PeriodComparison({ data }: PeriodComparisonProps) { {row.label} {row.b} - + {row.a} {row.delta.hasData ? ( void + onNext?: () => void onClose: () => void } -export function DrillDownModal({ day, contextData = [], open, onClose }: DrillDownModalProps) { +type PeriodKind = 'day' | 'month' | 'year' + +function getPeriodKind(date: string): PeriodKind { + if (/^\d{4}$/.test(date)) return 'year' + if (/^\d{4}-\d{2}$/.test(date)) return 'month' + return 'day' +} + +function getEntryTokenTotal(entry: DailyUsage): number { + return ( + entry.cacheReadTokens + + entry.cacheCreationTokens + + entry.inputTokens + + entry.outputTokens + + entry.thinkingTokens + ) +} + +function toPerMillion(cost: number, tokens: number): number | null { + return tokens > 0 ? cost / (tokens / 1_000_000) : null +} + +function toPerRequest(value: number, requests: number): number | null { + return requests > 0 ? value / requests : null +} + +function getDelta(current: number, reference: number | null) { + if (reference === null) return null + + const absolute = current - reference + const percent = reference !== 0 ? (absolute / reference) * 100 : null + + return { absolute, percent } +} + +function formatDeltaValue( + delta: ReturnType, + formatter: (value: number) => string, + fallback = '–', +) { + if (!delta) return fallback + if (delta.absolute === 0) return `→ ${formatter(0)}` + + return `${delta.absolute > 0 ? '↑' : '↓'} ${formatter(Math.abs(delta.absolute))}` +} + +function formatDeltaPercent(delta: ReturnType, fallback = '–') { + if (!delta) return fallback + if (delta.percent === null) return fallback + if (delta.percent === 0) return '0.0%' + + return `${delta.percent > 0 ? '+' : ''}${delta.percent.toFixed(1)}%` +} + +function isEditableTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) return false + + const tagName = target.tagName + return ( + target.isContentEditable || + tagName === 'INPUT' || + tagName === 'TEXTAREA' || + tagName === 'SELECT' + ) +} + +function getBenchmarkWindowLabel(count: number, unitLabel: string) { + return `${count}${unitLabel}` +} + +/** Renders the per-period drilldown dialog with navigation and benchmarks. */ +export function DrillDownModal({ + day, + contextData = [], + open, + hasPrevious: hasPreviousProp, + hasNext: hasNextProp, + currentIndex: currentIndexProp, + totalCount: totalCountProp, + onPrevious, + onNext, + onClose, +}: DrillDownModalProps) { const { t } = useTranslation() + + const periodKind = day ? getPeriodKind(day.date) : 'day' + + const sortedContextData = useMemo( + () => [...contextData].sort((a, b) => a.date.localeCompare(b.date)), + [contextData], + ) + + const contextIndex = useMemo( + () => (day ? sortedContextData.findIndex((entry) => entry.date === day.date) : -1), + [day, sortedContextData], + ) + + const previousEntry = contextIndex > 0 ? sortedContextData[contextIndex - 1] : null + const previousSeven = + contextIndex > 0 ? sortedContextData.slice(Math.max(0, contextIndex - 7), contextIndex) : [] + const hasPrevious = hasPreviousProp ?? contextIndex > 0 + const hasNext = hasNextProp ?? (contextIndex >= 0 && contextIndex < sortedContextData.length - 1) + const currentIndex = currentIndexProp ?? (contextIndex >= 0 ? contextIndex + 1 : 0) + const totalCount = totalCountProp ?? sortedContextData.length + + const tokensTotal = day ? getEntryTokenTotal(day) : 0 + const hasTokens = tokensTotal > 0 + const modelData = useMemo(() => { if (!day) return [] + const map = new Map< string, { + provider: string cost: number tokens: number input: number @@ -44,9 +166,12 @@ export function DrillDownModal({ day, contextData = [], open, onClose }: DrillDo requests: number } >() + for (const mb of day.modelBreakdowns) { const name = normalizeModelName(mb.modelName) - const ex = map.get(name) ?? { + const provider = getModelProvider(mb.modelName) + const existing = map.get(name) ?? { + provider, cost: 0, tokens: 0, input: 0, @@ -56,54 +181,91 @@ export function DrillDownModal({ day, contextData = [], open, onClose }: DrillDo thinking: 0, requests: 0, } - ex.cost += mb.cost - ex.tokens += + + existing.cost += mb.cost + existing.tokens += mb.inputTokens + mb.outputTokens + mb.cacheCreationTokens + mb.cacheReadTokens + mb.thinkingTokens - ex.input += mb.inputTokens - ex.output += mb.outputTokens - ex.cacheRead += mb.cacheReadTokens - ex.cacheCreate += mb.cacheCreationTokens - ex.thinking += mb.thinkingTokens - ex.requests += mb.requestCount - map.set(name, ex) + existing.input += mb.inputTokens + existing.output += mb.outputTokens + existing.cacheRead += mb.cacheReadTokens + existing.cacheCreate += mb.cacheCreationTokens + existing.thinking += mb.thinkingTokens + existing.requests += mb.requestCount + + map.set(name, existing) } + return Array.from(map.entries()) - .map(([name, v]) => ({ name, ...v })) + .map(([name, value]) => ({ + name, + ...value, + costShare: day.totalCost > 0 ? (value.cost / day.totalCost) * 100 : 0, + tokenShare: tokensTotal > 0 ? (value.tokens / tokensTotal) * 100 : 0, + costPerMillion: toPerMillion(value.cost, value.tokens), + costPerRequest: toPerRequest(value.cost, value.requests), + tokensPerRequest: toPerRequest(value.tokens, value.requests), + })) .sort((a, b) => b.cost - a.cost) - }, [day]) + }, [day, tokensTotal]) - if (!day) return null + const providerData = useMemo(() => { + const map = new Map< + string, + { cost: number; tokens: number; requests: number; activeModels: Set } + >() - const tokensTotal = - day.cacheReadTokens + - day.cacheCreationTokens + - day.inputTokens + - day.outputTokens + - day.thinkingTokens - const hasTokens = tokensTotal > 0 + for (const model of modelData) { + const existing = map.get(model.provider) ?? { + cost: 0, + tokens: 0, + requests: 0, + activeModels: new Set(), + } - const cacheRate = hasTokens ? (day.cacheReadTokens / tokensTotal) * 100 : 0 + existing.cost += model.cost + existing.tokens += model.tokens + existing.requests += model.requests + existing.activeModels.add(model.name) + map.set(model.provider, existing) + } - const pieData = modelData.map((m) => ({ name: m.name, value: m.cost })) - const avgTokensPerRequest = day.requestCount > 0 ? tokensTotal / day.requestCount : 0 - const avgCostPerRequest = day.requestCount > 0 ? day.totalCost / day.requestCount : 0 - const costPerMillion = hasTokens ? day.totalCost / (tokensTotal / 1_000_000) : null + return Array.from(map.entries()) + .map(([provider, value]) => ({ + provider, + cost: value.cost, + tokens: value.tokens, + requests: value.requests, + activeModels: value.activeModels.size, + costShare: day && day.totalCost > 0 ? (value.cost / day.totalCost) * 100 : 0, + })) + .sort((a, b) => b.cost - a.cost) + }, [day, modelData]) + + if (!day) return null + + const pieData = modelData.map((model) => ({ name: model.name, value: model.cost })) + const cacheRate = hasTokens ? (day.cacheReadTokens / tokensTotal) * 100 : 0 + const avgTokensPerRequest = toPerRequest(tokensTotal, day.requestCount) + const avgCostPerRequest = toPerRequest(day.totalCost, day.requestCount) + const costPerMillion = toPerMillion(day.totalCost, tokensTotal) + const hasRequestCounts = + day.requestCount > 0 || + day.modelBreakdowns.some((modelBreakdown) => modelBreakdown.requestCount > 0) || + contextData.some((entry) => entry.requestCount > 0) const costRanking = [...contextData] .sort((a, b) => b.totalCost - a.totalCost) .findIndex((entry) => entry.date === day.date) + 1 - const requestRanking = - [...contextData] - .sort((a, b) => b.requestCount - a.requestCount) - .findIndex((entry) => entry.date === day.date) + 1 - const previousSeven = [...contextData] - .filter((entry) => entry.date < day.date) - .sort((a, b) => a.date.localeCompare(b.date)) - .slice(-7) + const requestRanking = hasRequestCounts + ? [...contextData] + .sort((a, b) => b.requestCount - a.requestCount) + .findIndex((entry) => entry.date === day.date) + 1 + : 0 + const avgCost7 = previousSeven.length > 0 ? previousSeven.reduce((sum, entry) => sum + entry.totalCost, 0) / previousSeven.length @@ -112,15 +274,164 @@ export function DrillDownModal({ day, contextData = [], open, onClose }: DrillDo previousSeven.length > 0 ? previousSeven.reduce((sum, entry) => sum + entry.requestCount, 0) / previousSeven.length : null - const topRequestModel = modelData.reduce( + const avgTokens7 = + previousSeven.length > 0 + ? previousSeven.reduce((sum, entry) => sum + getEntryTokenTotal(entry), 0) / + previousSeven.length + : null + const avgCostPerMillion7 = + previousSeven.length > 0 + ? toPerMillion( + previousSeven.reduce((sum, entry) => sum + entry.totalCost, 0), + previousSeven.reduce((sum, entry) => sum + getEntryTokenTotal(entry), 0), + ) + : null + const benchmarkWindowLabel = getBenchmarkWindowLabel( + previousSeven.length > 0 ? previousSeven.length : 7, + t(`drillDown.windowUnit.${periodKind}`), + ) + + const previousTokens = previousEntry ? getEntryTokenTotal(previousEntry) : null + const previousCostPerMillion = previousEntry + ? toPerMillion(previousEntry.totalCost, getEntryTokenTotal(previousEntry)) + : null + + const topCostModel = modelData[0] ?? null + const topRequestModel = hasRequestCounts + ? modelData.reduce( + (best, current) => (!best || current.requests > best.requests ? current : best), + null as (typeof modelData)[number] | null, + ) + : null + const topTokenModel = modelData.reduce( + (best, current) => (!best || current.tokens > best.tokens ? current : best), + null as (typeof modelData)[number] | null, + ) + const priciestPerMillionModel = modelData.reduce( (best, current) => { - if (!best || current.requests > best.requests) return current + if (current.costPerMillion === null) return best + if (!best || best.costPerMillion === null || current.costPerMillion > best.costPerMillion) { + return current + } return best }, null as (typeof modelData)[number] | null, ) - const formatTokenShare = (value: number) => - hasTokens ? formatPercent((value / tokensTotal) * 100) : '–' + + const topThreeCostShare = + day.totalCost > 0 + ? (modelData.slice(0, 3).reduce((sum, model) => sum + model.cost, 0) / day.totalCost) * 100 + : 0 + + const summaryCards = [ + { label: t('common.tokens'), value: }, + { + label: '$/1M', + value: + costPerMillion !== null ? : '–', + }, + { + label: t('common.requests'), + value: , + }, + { label: t('common.models'), value: formatNumber(modelData.length) }, + { label: t('drillDown.activeProviders'), value: formatNumber(providerData.length) }, + { + label: t('drillDown.tokensPerRequest'), + value: + avgTokensPerRequest !== null ? ( + + ) : ( + '–' + ), + }, + { + label: t('drillDown.costPerRequest'), + value: + avgCostPerRequest !== null ? ( + + ) : ( + '–' + ), + }, + { + label: t('drillDown.cacheRate'), + value: , + }, + { + label: t('common.thinking'), + value: , + }, + { label: t('drillDown.costRank'), value: costRanking > 0 ? `#${costRanking}` : '–' }, + { label: t('drillDown.requestRank'), value: requestRanking > 0 ? `#${requestRanking}` : '–' }, + { + label: t('drillDown.coverage'), + value: + (day._aggregatedDays ?? 1) > 1 + ? t('drillDown.coverageDays', { count: day._aggregatedDays ?? 1 }) + : t('drillDown.singlePeriod', { period: t(`periods.${periodKind}`) }), + }, + ] + + const benchmarkCards = [ + { + label: t('drillDown.costVsPrevious'), + primary: formatDeltaValue( + getDelta(day.totalCost, previousEntry?.totalCost ?? null), + formatCurrency, + ), + secondary: formatDeltaPercent(getDelta(day.totalCost, previousEntry?.totalCost ?? null)), + }, + { + label: t('drillDown.tokensVsPrevious'), + primary: formatDeltaValue(getDelta(tokensTotal, previousTokens), formatTokens), + secondary: formatDeltaPercent(getDelta(tokensTotal, previousTokens)), + }, + { + label: t('drillDown.requestsVsPrevious'), + primary: formatDeltaValue( + getDelta(day.requestCount, previousEntry?.requestCount ?? null), + (value) => formatNumber(Math.round(value)), + ), + secondary: formatDeltaPercent( + getDelta(day.requestCount, previousEntry?.requestCount ?? null), + ), + }, + { + label: t('drillDown.costPerMillionVsAverageWindow', { window: benchmarkWindowLabel }), + primary: formatDeltaValue( + costPerMillion !== null ? getDelta(costPerMillion, avgCostPerMillion7) : null, + formatCurrency, + ), + secondary: avgCostPerMillion7 !== null ? formatCurrency(avgCostPerMillion7) : '–', + }, + { + label: t('drillDown.costVsAverageWindow', { window: benchmarkWindowLabel }), + primary: formatDeltaValue(getDelta(day.totalCost, avgCost7), formatCurrency), + secondary: avgCost7 !== null ? formatCurrency(avgCost7) : '–', + }, + { + label: t('drillDown.requestsVsAverageWindow', { window: benchmarkWindowLabel }), + primary: formatDeltaValue(getDelta(day.requestCount, avgRequests7), (value) => + formatNumber(Math.round(value)), + ), + secondary: avgRequests7 !== null ? formatNumber(Math.round(avgRequests7)) : '–', + }, + { + label: t('drillDown.tokensVsAverageWindow', { window: benchmarkWindowLabel }), + primary: formatDeltaValue(getDelta(tokensTotal, avgTokens7), formatTokens), + secondary: avgTokens7 !== null ? formatTokens(avgTokens7) : '–', + }, + { + label: t('drillDown.costPerMillionVsPrevious'), + primary: formatDeltaValue( + costPerMillion !== null ? getDelta(costPerMillion, previousCostPerMillion) : null, + formatCurrency, + ), + secondary: previousCostPerMillion !== null ? formatCurrency(previousCostPerMillion) : '–', + }, + ] + const tokenSegments = [ { id: 'cacheRead', @@ -149,205 +460,428 @@ export function DrillDownModal({ day, contextData = [], open, onClose }: DrillDo }, ] as const + const topModelCards = [ + { + label: t('drillDown.topCostModel'), + title: topCostModel?.name ?? '–', + value: topCostModel ? formatPercent(topCostModel.costShare) : '–', + }, + { + label: t('drillDown.topRequestModel'), + title: topRequestModel?.name ?? '–', + value: + topRequestModel && topRequestModel.requests > 0 + ? t('drillDown.requestCountShort', { count: topRequestModel.requests }) + : '–', + }, + { + label: t('drillDown.topTokenModel'), + title: topTokenModel?.name ?? '–', + value: topTokenModel ? formatPercent(topTokenModel.tokenShare) : '–', + }, + { + label: t('drillDown.priciestPerMillionModel'), + title: priciestPerMillionModel?.name ?? '–', + value: + priciestPerMillionModel?.costPerMillion !== null && + priciestPerMillionModel?.costPerMillion !== undefined ? ( + + ) : ( + '–' + ), + }, + { + label: t('drillDown.topCostShare'), + title: topCostModel ? formatPercent(topCostModel.costShare) : '–', + value: topCostModel?.provider ?? '–', + }, + { + label: t('drillDown.topThreeCostShare'), + title: formatPercent(topThreeCostShare), + value: t('drillDown.modelCount', { count: Math.min(modelData.length, 3) }), + }, + ] + + const previousLabel = t( + periodKind === 'day' ? 'drillDown.previousDay' : 'drillDown.previousPeriod', + ) + const nextLabel = t(periodKind === 'day' ? 'drillDown.nextDay' : 'drillDown.nextPeriod') + + const handleDialogKeyDown = (event: KeyboardEvent) => { + if ( + event.altKey || + event.ctrlKey || + event.metaKey || + event.shiftKey || + isEditableTarget(event.target) + ) { + return + } + + if (event.key === 'ArrowLeft' && hasPrevious) { + event.preventDefault() + onPrevious?.() + } + + if (event.key === 'ArrowRight' && hasNext) { + event.preventDefault() + onNext?.() + } + } + return ( - !o && onClose()}> - + !isOpen && onClose()}> + - - {formatDate(day.date, 'long')} — {formatCurrency(day.totalCost)} - - {t('drillDown.description')} - - -
    -
    -
    {t('common.tokens')}
    -
    - -
    -
    -
    -
    $/1M
    -
    - {costPerMillion !== null ? ( - - ) : ( - '–' - )} -
    -
    -
    -
    {t('drillDown.cacheRate')}
    -
    - -
    -
    -
    -
    {t('common.models')}
    -
    {modelData.length}
    -
    -
    -
    {t('common.requests')}
    -
    - +
    +
    + + {formatDate(day.date, 'long')} — {formatCurrency(day.totalCost)} + + + {t('drillDown.description', { periodType: t(`periods.${periodKind}`) })} +
    -
    -
    -
    {t('common.thinking')}
    -
    - -
    -
    -
    -
    {t('drillDown.tokensPerRequest')}
    -
    - -
    -
    -
    -
    {t('drillDown.costPerRequest')}
    -
    - -
    -
    -
    -
    {t('drillDown.costRank')}
    -
    {costRanking > 0 ? `#${costRanking}` : '–'}
    -
    -
    -
    {t('drillDown.requestRank')}
    -
    - {requestRanking > 0 ? `#${requestRanking}` : '–'} -
    -
    -
    -
    -
    -
    {t('drillDown.topRequestModel')}
    -
    {topRequestModel?.name ?? '–'}
    -
    -
    -
    {t('drillDown.costVsAverage7d')}
    -
    - {avgCost7 !== null - ? `${day.totalCost >= avgCost7 ? '↑' : '↓'} ${formatCurrency(Math.abs(day.totalCost - avgCost7))}` - : '–'} +
    +
    + + +
    +
    + {t('drillDown.position', { current: currentIndex, total: totalCount })} + + {t('drillDown.keyboardHint')} +
    -
    -
    {t('drillDown.requestsVsAverage7d')}
    -
    - {avgRequests7 !== null - ? `${day.requestCount >= avgRequests7 ? '↑' : '↓'} ${Math.abs(day.requestCount - avgRequests7).toFixed(0)}` - : '–'} -
    +
    + + {t('drillDown.periodType', { period: t(`periods.${periodKind}`) })} + + {(day._aggregatedDays ?? 1) > 1 && ( + + {t('drillDown.coverageDays', { count: day._aggregatedDays ?? 1 })} + + )}
    -
    + - {/* Token type stacked bar */} -
    -
    - {t('drillDown.tokenDistribution')} -
    -
    - {hasTokens && - tokenSegments.map((seg) => ( -
    - ))} +
    +

    {t('drillDown.overview')}

    +
    + {summaryCards.map((card) => ( +
    +
    {card.label}
    +
    {card.value}
    +
    + ))}
    -
    - {tokenSegments.map((segment) => ( - - - {segment.label} {formatTokenShare(segment.value)} - +
    + +
    +

    {t('drillDown.benchmarks')}

    +
    + {benchmarkCards.map((card) => ( +
    +
    {card.label}
    +
    {card.primary}
    +
    {card.secondary}
    +
    ))}
    -
    - -
    -
    - - - - {pieData.map((entry) => ( - - ))} - - formatCurrency(v)} />} /> - - + + +
    +
    +

    {t('drillDown.modelBreakdown')}

    + + {t('drillDown.modelCount', { count: modelData.length })} +
    -
    - {modelData.map((model) => { - const share = day.totalCost > 0 ? (model.cost / day.totalCost) * 100 : 0 - return ( +
    +
    +
    + {topModelCards.map((card) => ( +
    +
    {card.label}
    +
    {card.title}
    +
    {card.value}
    +
    + ))} +
    + + {pieData.length > 0 && ( +
    +
    + {t('drillDown.costShareByModel')} +
    + + + + {pieData.map((entry) => ( + + ))} + + formatCurrency(value)} />} + /> + + +
    + )} +
    + +
    + {modelData.map((model) => (
    -
    - - {model.name} - - {getModelProvider(model.name)} - - - {formatPercent(share)} - +
    +
    + + {model.name} + + {model.provider} + +
    +
    + {t('drillDown.requestCountShort', { count: model.requests })} +
    -
    -
    - + +
    +
    +
    {t('tables.recentDays.cost')}
    +
    - - +
    +
    +
    +
    {t('drillDown.costShare')}
    +
    {formatPercent(model.costShare)}
    +
    +
    +
    {t('tables.recentDays.tokens')}
    +
    - - - {t('drillDown.requestCountShort', { count: model.requests })} - +
    +
    +
    +
    {t('drillDown.tokenShare')}
    +
    {formatPercent(model.tokenShare)}
    +
    +
    +
    {t('common.requests')}
    +
    + +
    +
    +
    +
    $/1M
    +
    + {model.costPerMillion !== null ? ( + + ) : ( + '–' + )} +
    +
    +
    +
    {t('drillDown.costPerRequest')}
    +
    + {model.costPerRequest !== null ? ( + + ) : ( + '–' + )} +
    +
    +
    +
    {t('drillDown.tokensPerRequest')}
    +
    + {model.tokensPerRequest !== null ? ( + + ) : ( + '–' + )} +
    -
    - {model.requests > 0 - ? t('drillDown.modelRequestSummary', { - costPerRequest: formatCurrency(model.cost / model.requests), - tokensPerRequest: formatTokens(model.tokens / model.requests), - }) - : t('drillDown.noRequests')} +
    + +
    +
    +
    {t('common.input')}
    +
    {formatTokens(model.input)}
    +
    +
    +
    {t('common.output')}
    +
    {formatTokens(model.output)}
    +
    +
    +
    {t('common.cacheRead')}
    +
    {formatTokens(model.cacheRead)}
    +
    +
    +
    {t('common.cacheWrite')}
    +
    {formatTokens(model.cacheCreate)}
    +
    +
    +
    {t('common.thinking')}
    +
    {formatTokens(model.thinking)}
    - ) - })} + ))} +
    +
    +
    + +
    +
    +

    {t('drillDown.providerSummary')}

    + + {t('drillDown.providerCount', { count: providerData.length })} + +
    + +
    + {providerData.map((provider) => ( +
    +
    + + {provider.provider} + + + {t('drillDown.activeModelsCount', { count: provider.activeModels })} + +
    +
    +
    +
    {t('tables.recentDays.cost')}
    +
    + +
    +
    +
    +
    {t('drillDown.costShare')}
    +
    {formatPercent(provider.costShare)}
    +
    +
    +
    {t('tables.recentDays.tokens')}
    +
    + +
    +
    +
    +
    {t('common.requests')}
    +
    + +
    +
    +
    +
    + ))} +
    +
    + +
    +

    {t('drillDown.tokenDistribution')}

    +
    +
    + {hasTokens && + tokenSegments.map((segment) => { + const share = ((segment.value / tokensTotal) * 100).toFixed(1) + const segmentLabel = `${segment.label}: ${formatTokens(segment.value)} (${share}%)` + + return ( +
    + ) + })} +
    +
    + {tokenSegments.map((segment) => ( +
    +
    + + {segment.label} +
    +
    {formatTokens(segment.value)}
    +
    + {hasTokens ? formatPercent((segment.value / tokensTotal) * 100) : '–'} +
    +
    + ))} +
    -
    +
    ) diff --git a/src/components/features/forecast/CostForecast.tsx b/src/components/features/forecast/CostForecast.tsx index fd14b8b..a80298c 100644 --- a/src/components/features/forecast/CostForecast.tsx +++ b/src/components/features/forecast/CostForecast.tsx @@ -12,6 +12,7 @@ import { Legend, } from 'recharts' import { ChartCard, ChartAnimationAware, ChartReveal } from '@/components/charts/ChartCard' +import { ChartLegend } from '@/components/charts/ChartLegend' import { CustomTooltip } from '@/components/charts/CustomTooltip' import { CHART_COLORS, CHART_MARGIN, CHART_ANIMATION } from '@/components/charts/chart-theme' import { coerceNumber, formatCurrency, formatDateAxis } from '@/lib/formatters' @@ -27,6 +28,7 @@ interface CostForecastProps { viewMode?: ViewMode } +/** Renders the current-month cost forecast card. */ export function CostForecast({ data, viewMode = 'daily' }: CostForecastProps) { const { t } = useTranslation() const { @@ -132,9 +134,9 @@ export function CostForecast({ data, viewMode = 'daily' }: CostForecastProps) { if (data.length === 0) { return (
    -
    - -

    {t('forecast.noData')}

    +
    + +

    {t('forecast.noData')}

    ) @@ -161,10 +163,10 @@ export function CostForecast({ data, viewMode = 'daily' }: CostForecastProps) { if (chartData.length === 0) { return (
    -
    - -

    {t('forecast.noForecast')}

    -

    {t('forecast.requiresTwoDays')}

    +
    + +

    {t('forecast.noForecast')}

    +

    {t('forecast.requiresTwoDays')}

    ) @@ -177,7 +179,7 @@ export function CostForecast({ data, viewMode = 'daily' }: CostForecastProps) { {t('forecast.monthEndForecast')}{' '} {t(`forecast.${confidence}`)} @@ -203,7 +205,7 @@ export function CostForecast({ data, viewMode = 'daily' }: CostForecastProps) { > {(animate) => ( - + @@ -231,7 +233,7 @@ export function CostForecast({ data, viewMode = 'daily' }: CostForecastProps) { axisLine={false} /> formatCurrency(v)} />} /> - + } /> ()) const [tooltip, setTooltip] = useState<{ x: number y: number @@ -158,6 +175,102 @@ export function HeatmapCalendar({ }, [config, data, locale]) const todayStr = localToday() + const axisColor = 'hsl(var(--muted-foreground))' + const todayOutlineColor = 'hsl(var(--primary))' + const [focusedDate, setFocusedDate] = useState(null) + const scheduleFocus = useCallback((callback: () => void) => { + if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { + window.requestAnimationFrame(callback) + return + } + setTimeout(callback, 0) + }, []) + const availableDates = useMemo(() => cells.map((cell) => cell.date), [cells]) + const cellRows = useMemo( + () => Array.from({ length: 7 }, (_, day) => cells.filter((cell) => cell.day === day)), + [cells], + ) + const defaultFocusedDate = useMemo( + () => + (availableDates.includes(todayStr) ? todayStr : undefined) ?? + cells.find((cell) => cell.value > 0)?.date ?? + availableDates[0] ?? + null, + [availableDates, cells, todayStr], + ) + + const focusDate = useCallback( + (nextDate: string | null) => { + if (!nextDate) return + setFocusedDate(nextDate) + scheduleFocus(() => { + dayButtonRefs.current.get(nextDate)?.focus() + }) + }, + [scheduleFocus], + ) + + useEffect(() => { + if (!defaultFocusedDate) return + if (!focusedDate || !availableDates.includes(focusedDate)) { + setFocusedDate(defaultFocusedDate) + } + }, [availableDates, defaultFocusedDate, focusedDate]) + + const handleCellKeyDown = useCallback( + (event: ReactKeyboardEvent, currentDate: string) => { + const currentCell = cells.find((cell) => cell.date === currentDate) + if (!currentCell) return + + const currentRow = currentCell.day + const currentColumn = currentCell.week + + const moveToCell = (rowIndex: number, columnIndex: number) => { + const targetRow = cellRows[Math.max(0, Math.min(rowIndex, cellRows.length - 1))] + if (!targetRow || targetRow.length === 0) return + + const nextCell = targetRow[Math.max(0, Math.min(columnIndex, targetRow.length - 1))] + focusDate(nextCell?.date ?? null) + } + + const moveToRowBoundary = (targetColumn: 0 | 'end') => { + const row = cellRows[currentRow] + if (!row || row.length === 0) return + const nextCell = targetColumn === 0 ? row[0] : row[row.length - 1] + focusDate(nextCell?.date ?? null) + } + + switch (event.key) { + case 'ArrowLeft': + event.preventDefault() + moveToCell(currentRow, currentColumn - 1) + break + case 'ArrowRight': + event.preventDefault() + moveToCell(currentRow, currentColumn + 1) + break + case 'ArrowUp': + event.preventDefault() + moveToCell(currentRow - 1, currentColumn) + break + case 'ArrowDown': + event.preventDefault() + moveToCell(currentRow + 1, currentColumn) + break + case 'Home': + event.preventDefault() + moveToRowBoundary(0) + break + case 'End': + event.preventDefault() + moveToRowBoundary('end') + break + default: + break + } + }, + [cellRows, cells, focusDate], + ) // Heatmap only makes sense for daily view if (viewMode !== 'daily') { @@ -173,7 +286,7 @@ export function HeatmapCalendar({

    {config.empty}

    -

    +

    {t('charts.heatmap.switchToDaily')}

    @@ -197,9 +310,17 @@ export function HeatmapCalendar({ -
    +
    - + {/* Day labels */} {dayLabels.map( (label, i) => @@ -209,7 +330,7 @@ export function HeatmapCalendar({ x={0} y={TOP_GUTTER + i * TOTAL + CELL_SIZE - 2} fontSize={9} - fill="hsl(220, 8%, 46%)" + fill={axisColor} className="font-mono" > {label} @@ -224,7 +345,7 @@ export function HeatmapCalendar({ x={LEFT_GUTTER + m.week * TOTAL} y={12} fontSize={9} - fill="hsl(220, 8%, 46%)" + fill={axisColor} className="font-mono" > {m.label} @@ -232,91 +353,106 @@ export function HeatmapCalendar({ ))} {/* Cells */} - {cells.map((cell, i) => { - const isToday = cell.date === todayStr - const formattedDate = fullDateFormatter.format(new Date(`${cell.date}T00:00:00`)) - const accessibleLabel = t('charts.heatmap.cellLabel', { - date: formattedDate, - value: config.formatter(cell.value), - }) - return ( - - { - const bounds = overlayRef.current?.getBoundingClientRect() - if (!bounds) return - setTooltip({ - x: event.clientX - bounds.left, - y: event.clientY - bounds.top - 12, - date: cell.date, - value: cell.value, - }) - }} - onFocus={(event) => { - const bounds = overlayRef.current?.getBoundingClientRect() - if (!bounds) return - const rect = event.currentTarget.getBoundingClientRect() - setTooltip({ - x: rect.left - bounds.left + rect.width / 2, - y: rect.top - bounds.top - 8, - date: cell.date, - value: cell.value, - }) - }} - onBlur={() => setTooltip(null)} - onMouseLeave={() => setTooltip(null)} - > - {accessibleLabel} - - {isToday && ( - - )} - - ) - })} + {cellRows.map((row, rowIndex) => ( + + {row.map((cell) => { + const isToday = cell.date === todayStr + const formattedDate = fullDateFormatter.format( + new Date(`${cell.date}T00:00:00`), + ) + const accessibleLabel = t('charts.heatmap.cellLabel', { + date: formattedDate, + value: config.formatter(cell.value), + }) + + return ( + + { + if (node) dayButtonRefs.current.set(cell.date, node) + else dayButtonRefs.current.delete(cell.date) + }} + x={LEFT_GUTTER + cell.week * TOTAL} + y={TOP_GUTTER + cell.day * TOTAL} + width={CELL_SIZE} + height={CELL_SIZE} + rx={2} + fill={getColor(cell.value, maxValue, config.hue, isDark)} + stroke="transparent" + strokeWidth={1.5} + className="transition-all duration-150 focus-visible:stroke-primary" + tabIndex={focusedDate === cell.date ? 0 : -1} + role="gridcell" + aria-label={accessibleLabel} + aria-current={isToday ? 'date' : undefined} + onKeyDown={(event) => handleCellKeyDown(event, cell.date)} + onMouseEnter={(event) => { + const bounds = overlayRef.current?.getBoundingClientRect() + if (!bounds) return + setTooltip({ + x: event.clientX - bounds.left, + y: event.clientY - bounds.top - 12, + date: formattedDate, + value: cell.value, + }) + }} + onFocus={(event) => { + setFocusedDate(cell.date) + const bounds = overlayRef.current?.getBoundingClientRect() + if (!bounds) return + const rect = event.currentTarget.getBoundingClientRect() + setTooltip({ + x: rect.left - bounds.left + rect.width / 2, + y: rect.top - bounds.top - 8, + date: formattedDate, + value: cell.value, + }) + }} + onBlur={() => setTooltip(null)} + onMouseLeave={() => setTooltip(null)} + > + {accessibleLabel} + + {isToday && ( + + )} + + ) + })} + + ))}
    {tooltip && (
    {config.formatter(tooltip.value)} - {tooltip.date} + {tooltip.date}
    )} {/* Legend */} -
    +
    {t('charts.heatmap.less')} {[0, 0.15, 0.3, 0.45, 0.6, 0.75, 0.9, 1].map((level, i) => (
    ))} {t('charts.heatmap.more')} diff --git a/src/components/features/help/HelpPanel.tsx b/src/components/features/help/HelpPanel.tsx index a3543bc..5e1a7ab 100644 --- a/src/components/features/help/HelpPanel.tsx +++ b/src/components/features/help/HelpPanel.tsx @@ -33,6 +33,7 @@ const FEATURE_KEYS: Array = [ const TABLE_KEYS: Array = ['providerEfficiency', 'modelEfficiency', 'recentDays'] +/** Renders the contextual help dialog for dashboard concepts. */ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { const { t } = useTranslation() const shortcuts = getKeyboardShortcuts() @@ -113,7 +114,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { return ( - + {t('header.help')} {t('commandPalette.description')} @@ -133,7 +134,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { href={NPM_PACKAGE_URL} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center rounded-md border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" + className="inline-flex items-center rounded-md border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none" > {t('helpPanel.projectLinks.npm')} @@ -141,7 +142,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center rounded-md border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" + className="inline-flex items-center rounded-md border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none" > {t('helpPanel.projectLinks.github')} @@ -149,7 +150,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { href={GITHUB_ISSUES_URL} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center rounded-md border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" + className="inline-flex items-center rounded-md border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none" > {t('helpPanel.projectLinks.issues')} @@ -158,7 +159,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {/* Keyboard shortcuts */}
    -
    +

    {t('header.help')}

    @@ -181,7 +182,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {/* Metric explanations */}
    -
    +

    {t('dashboard.metrics.title')}

    @@ -189,7 +190,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {Object.entries(METRIC_HELP).map(([key, description]) => (

    {metricLabels[key] ?? key}

    -

    {description}

    +

    {description}

    ))}
    @@ -199,7 +200,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {/* Chart explanations */}
    -
    +

    {t('helpPanel.chartsAndFeatures')}

    @@ -207,7 +208,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {chartKeys.map((key) => (

    {chartLabels[key]}

    -

    {CHART_HELP[key]}

    +

    {CHART_HELP[key]}

    ))}
    @@ -216,7 +217,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) {
    -
    +

    {t('helpPanel.dashboardSectionsTitle')}

    @@ -224,7 +225,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {sectionKeys.map((key) => (

    {sectionLabels[key]}

    -

    {SECTION_HELP[key]}

    +

    {SECTION_HELP[key]}

    ))}
    @@ -233,7 +234,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) {
    -
    +

    {t('helpPanel.featuresTitle')}

    @@ -241,7 +242,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {FEATURE_KEYS.map((key) => (

    {featureLabels[key]}

    -

    {FEATURE_HELP[key]}

    +

    {FEATURE_HELP[key]}

    ))}
    @@ -250,7 +251,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) {
    -
    +

    {t('helpPanel.tablesTitle')}

    @@ -258,7 +259,7 @@ export function HelpPanel({ open, onOpenChange }: HelpPanelProps) { {TABLE_KEYS.map((key) => (

    {featureLabels[key]}

    -

    {FEATURE_HELP[key]}

    +

    {FEATURE_HELP[key]}

    ))}
    diff --git a/src/components/features/help/InfoButton.tsx b/src/components/features/help/InfoButton.tsx index 1c4b968..6309a7c 100644 --- a/src/components/features/help/InfoButton.tsx +++ b/src/components/features/help/InfoButton.tsx @@ -8,6 +8,7 @@ interface InfoButtonProps { className?: string } +/** Renders a compact info trigger with tooltip content. */ export function InfoButton({ text, className }: InfoButtonProps) { const { t } = useTranslation() return ( @@ -18,7 +19,7 @@ export function InfoButton({ text, className }: InfoButtonProps) { aria-label={t('common.showInfo')} data-info-button="true" className={cn( - 'inline-flex items-center justify-center text-muted-foreground/50 hover:text-muted-foreground transition-colors', + 'inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground/50 transition-colors hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none', className, )} > diff --git a/src/components/features/help/InfoHeading.tsx b/src/components/features/help/InfoHeading.tsx index d4769dc..5da3997 100644 --- a/src/components/features/help/InfoHeading.tsx +++ b/src/components/features/help/InfoHeading.tsx @@ -8,9 +8,10 @@ interface InfoHeadingProps { className?: string | undefined } +/** Renders a heading paired with contextual help text. */ export function InfoHeading({ children, info, className }: InfoHeadingProps) { return ( -
    +
    {children} {info && }
    diff --git a/src/components/features/insights/UsageInsights.tsx b/src/components/features/insights/UsageInsights.tsx index 70e84d4..bd58dec 100644 --- a/src/components/features/insights/UsageInsights.tsx +++ b/src/components/features/insights/UsageInsights.tsx @@ -36,7 +36,7 @@ function InsightCard({ title, icon, value, summary, details }: InsightCardProps)
    -
    +
    {title}
    {value}
    @@ -52,7 +52,7 @@ function InsightCard({ title, icon, value, summary, details }: InsightCardProps) key={detail.label} className="rounded-xl border border-border/50 bg-muted/20 px-3 py-2" > -
    +
    {detail.label}
    {detail.value}
    @@ -63,6 +63,7 @@ function InsightCard({ title, icon, value, summary, details }: InsightCardProps) ) } +/** Renders the high-level narrative insight cards for the current slice. */ export function UsageInsights({ metrics, viewMode, totalCalendarDays }: UsageInsightsProps) { const { t } = useTranslation() const coverageRate = @@ -91,7 +92,7 @@ export function UsageInsights({ metrics, viewMode, totalCalendarDays }: UsageIns description={t('dashboard.insights.description')} info={SECTION_HELP.insights} /> -
    +
    - + {t('dashboard.insights.quickRead')} diff --git a/src/components/features/limits/ProviderLimitsSection.tsx b/src/components/features/limits/ProviderLimitsSection.tsx index 16e4197..68fd470 100644 --- a/src/components/features/limits/ProviderLimitsSection.tsx +++ b/src/components/features/limits/ProviderLimitsSection.tsx @@ -18,6 +18,7 @@ import { AlertTriangle, CreditCard, ShieldCheck, TrendingUp } from 'lucide-react import { Card, CardContent } from '@/components/ui/card' import { SectionHeader } from '@/components/ui/section-header' import { ChartAnimationAware, ChartCard, ChartReveal } from '@/components/charts/ChartCard' +import { ChartLegend } from '@/components/charts/ChartLegend' import { CHART_ANIMATION, CHART_COLORS, CHART_MARGIN } from '@/components/charts/chart-theme' import { buildProviderMonthlyCosts, getLatestMonth } from '@/lib/provider-limits' import i18n from '@/lib/i18n' @@ -87,6 +88,7 @@ function toTooltipNumber(value: TooltipValueType | undefined) { return Number.isFinite(numericValue) ? numericValue : 0 } +/** Renders provider limit progress, subscriptions, and timeline analysis. */ export function ProviderLimitsSection({ data, providers, @@ -277,7 +279,7 @@ export function ProviderLimitsSection({
    -
    +
    {item.label}
    {item.value}
    @@ -339,7 +341,7 @@ export function ProviderLimitsSection({
    -
    +
    {t('limits.tracks.usageFocusMonth')}
    @@ -347,7 +349,7 @@ export function ProviderLimitsSection({
    -
    +
    {t('limits.tracks.limitSubscription')}
    @@ -521,7 +523,7 @@ export function ProviderLimitsSection({ {row.monthlyLimit > 0 ? ( <>
    ) : (
    ) : ( {(animate) => ( - + @@ -891,7 +893,7 @@ export function ProviderLimitsSection({ background: 'color-mix(in srgb, hsl(var(--popover)) 90%, transparent)', }} /> - + } /> void } +/** Renders the PDF report generation action. */ export function PDFReportButton({ generating, onGenerate }: PDFReportProps) { const { t } = useTranslation() @@ -17,7 +18,7 @@ export function PDFReportButton({ generating, onGenerate }: PDFReportProps) { onClick={onGenerate} disabled={generating} title={t('commandPalette.commands.generateReport.label')} - className="h-11 flex-col gap-1 px-0 text-[10px] sm:h-9 sm:flex-row sm:gap-2 sm:px-3 sm:text-sm" + className="h-11 justify-start gap-2 px-3 text-xs sm:h-9 sm:text-sm" > {generating ? ( diff --git a/src/components/features/request-quality/RequestQuality.tsx b/src/components/features/request-quality/RequestQuality.tsx index 6a74777..a13c40b 100644 --- a/src/components/features/request-quality/RequestQuality.tsx +++ b/src/components/features/request-quality/RequestQuality.tsx @@ -1,10 +1,11 @@ import { useRef } from 'react' import { useTranslation } from 'react-i18next' -import { motion, useInView } from 'framer-motion' +import { useInView } from 'framer-motion' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { InfoHeading } from '@/components/features/help/InfoHeading' import { FEATURE_HELP } from '@/lib/help-content' import { formatCurrency, formatNumber, formatPercent, formatTokens } from '@/lib/formatters' +import { useShouldReduceMotion } from '@/lib/motion' import type { DashboardMetrics, ViewMode } from '@/types' interface RequestQualityProps { @@ -12,10 +13,12 @@ interface RequestQualityProps { viewMode: ViewMode } +/** Renders request-efficiency summary cards for the current slice. */ export function RequestQuality({ metrics, viewMode }: RequestQualityProps) { const { t } = useTranslation() const sectionRef = useRef(null) const inView = useInView(sectionRef, { once: true, amount: 0.25 }) + const shouldReduceMotion = useShouldReduceMotion() const cachePerRequest = metrics.totalRequests > 0 ? metrics.totalCacheRead / metrics.totalRequests : 0 const thinkingPerRequest = @@ -68,43 +71,35 @@ export function RequestQuality({ metrics, viewMode }: RequestQualityProps) { -
    +
    {qualityMetrics.map((item) => ( - -
    +
    +
    {item.label}
    {item.value}
    {item.hint}
    - 0 + ? `${Math.max(item.progress * 100, 6)}%` + : '0%' + : '0%', + }} />
    - +
    ))}
    -
    - -
    +
    +
    +
    {t('requestQuality.requestDensity')}
    @@ -120,28 +115,18 @@ export function RequestQuality({ metrics, viewMode }: RequestQualityProps) { : t('periods.day'), })}
    - - -
    +
    +
    +
    {t('requestQuality.cacheHitRate')}
    {formatPercent(metrics.cacheHitRate, 1)}
    {t('requestQuality.cacheHitHint')}
    - - -
    +
    +
    +
    {t('requestQuality.inputOutput')}
    @@ -150,17 +135,12 @@ export function RequestQuality({ metrics, viewMode }: RequestQualityProps) {
    {t('requestQuality.inputOutputHint')}
    - - -
    +
    +
    +
    {t('requestQuality.topRequestModel')}
    -
    +
    {metrics.topRequestModel?.name ?? '–'}
    @@ -168,7 +148,7 @@ export function RequestQuality({ metrics, viewMode }: RequestQualityProps) { ? `${formatNumber(metrics.topRequestModel.requests)} ${t('common.requests')}` : t('requestQuality.noRequestLeader')}
    - +
    diff --git a/src/components/features/risk/ConcentrationRisk.tsx b/src/components/features/risk/ConcentrationRisk.tsx index d8f8b10..fabb75e 100644 --- a/src/components/features/risk/ConcentrationRisk.tsx +++ b/src/components/features/risk/ConcentrationRisk.tsx @@ -18,6 +18,7 @@ function describeRisk(value: number) { return { label: 'low', tone: 'text-green-400 bg-green-400/10 border-green-400/20' } } +/** Renders concentration metrics for models and providers. */ export function ConcentrationRisk({ topModelShare, topProviderShare, @@ -38,11 +39,11 @@ export function ConcentrationRisk({ -
    +
    -
    +
    {t('risk.modelDependency')}
    {formatPercent(topModelShare, 1)}
    @@ -53,7 +54,7 @@ export function ConcentrationRisk({ {t(`risk.${modelRisk.label}`)}
    -
    +
    -
    +
    {t('risk.providerDependency')}
    @@ -79,7 +80,7 @@ export function ConcentrationRisk({ {t(`risk.${providerRisk.label}`)}
    -
    +
    ( null, ) + const titleRef = useRef(null) useEffect(() => { if (!open) return @@ -270,21 +274,29 @@ export function SettingsModal({ return ( - + { + event.preventDefault() + titleRef.current?.focus() + }} + > - {t('settings.modal.title')} + + {t('settings.modal.title')} + {t('settings.modal.description')}
    -
    +
    {t('settings.modal.dataStatus')}
    -
    +
    {t('settings.modal.lastLoaded')}
    @@ -292,13 +304,13 @@ export function SettingsModal({
    -
    +
    {t('settings.modal.loadedVia')}
    {loadSourceLabel}
    -
    +
    {t('settings.modal.cliAutoLoad')}
    @@ -369,7 +381,7 @@ export function SettingsModal({
    -
    +
    {t('settings.modal.defaultViewMode')}
    @@ -388,7 +400,7 @@ export function SettingsModal({
    -
    +
    {t('settings.modal.defaultDateRange')}
    @@ -409,7 +421,7 @@ export function SettingsModal({
    -
    +
    {t('settings.modal.filterProviders')}
    {providerOptions.length === 0 ? ( @@ -447,7 +459,7 @@ export function SettingsModal({
    -
    +
    {t('settings.modal.filterModels')}
    {modelOptions.length === 0 ? ( @@ -622,7 +634,7 @@ export function SettingsModal({ })) } className={cn( - 'inline-flex min-w-[88px] items-center justify-center rounded-full border px-3 py-1.5 text-xs font-medium uppercase tracking-[0.12em] transition-colors', + 'inline-flex min-w-[88px] items-center justify-center rounded-full border px-3 py-1.5 text-xs font-medium tracking-[0.12em] uppercase transition-colors', visible ? 'border-emerald-500/30 bg-emerald-500/10 text-foreground' : 'border-border bg-muted/20 text-muted-foreground hover:bg-accent hover:text-foreground', @@ -747,7 +759,7 @@ export function SettingsModal({
    {limitProviders.length === 0 ? ( -
    +
    {t('settings.modal.noProviders')}
    ) : ( @@ -795,7 +807,7 @@ export function SettingsModal({