From a4a87c374fab7d5d638bee185e10016f7131cad1 Mon Sep 17 00:00:00 2001 From: Thomas Gruender Date: Thu, 24 Sep 2026 23:10:26 +0200 Subject: [PATCH 1/4] feat(wizard): review fixes, 6-step flow, a11y, security and deps - fix undo/redo, Python injection and tooltip XSS - make pipeline options work; handle missing values - split step 4 into Projection + Glyphs & Color - add a11y and design tokens - upgrade Angular 21.2.24 and druidjs 0.9 (LLE hidden) - switch tests to Vitest; update CI to Node 24 - ship licence notices; update docs --- .github/workflows/deploy.yaml | 77 +- .gitignore | 6 +- .nvmrc | 1 + README.md | 86 +- ROADMAP.md | 22 +- angular.json | 35 +- docs/wizard.md | 80 + package-lock.json | 7040 +++++++---------- package.json | 53 +- src/app/canvas/glyph-canvas.component.ts | 2 +- .../canvas/magiclens/magiclens.component.ts | 2 +- .../services/canvas-renderer.service.ts | 7 +- .../settingscontrols.component.ts | 4 +- src/app/canvas/tooltip/tooltip.component.html | 19 +- src/app/canvas/tooltip/tooltip.component.scss | 33 + .../canvas/tooltip/tooltip.component.spec.ts | 58 + src/app/canvas/tooltip/tooltip.component.ts | 77 +- src/app/glyph/glyph-cache-object.ts | 2 +- src/app/menubar/menubar.component.html | 51 +- src/app/menubar/menubar.component.scss | 91 +- src/app/menubar/menubar.component.spec.ts | 77 + src/app/menubar/menubar.component.ts | 17 +- .../models/column-config.ts | 3 +- .../models/preprocessing-state.ts | 32 +- .../preprocessing-wizard.component.html | 170 +- .../preprocessing-wizard.component.scss | 300 +- .../preprocessing-wizard.component.spec.ts | 477 +- .../preprocessing-wizard.component.ts | 503 +- .../preprocessing-wizard.dialog.spec.ts | 277 + .../services/column-defaults.spec.ts | 122 + .../services/column-defaults.ts | 113 + .../services/history-diff.ts | 175 + .../services/preprocessing.service.spec.ts | 659 +- .../services/preprocessing.service.ts | 884 +-- ...preprocessing.service.worker-files.spec.ts | 133 + .../services/processing-config.ts | 121 + .../services/wizard-storage.spec.ts | 172 + .../services/wizard-storage.ts | 117 + .../shared/_wizard-shared.scss | 416 +- .../shared/constants/help-text.ts | 121 +- .../shared/constants/step-info.spec.ts | 66 + .../shared/constants/step-info.ts | 72 +- .../constants/wizard-error-classes.spec.ts | 70 +- .../shared/constants/wizard-error-classes.ts | 98 +- .../data-preview-table.component.html | 7 +- .../data-preview-table.component.scss | 14 +- .../data-preview-table.component.spec.ts | 36 + .../data-type-badge.component.scss | 1 - .../shared/dialog-focus.spec.ts | 121 + .../shared/dialog-focus.ts | 87 + .../help-tooltip/help-tooltip.component.html | 30 +- .../help-tooltip/help-tooltip.component.scss | 28 +- .../help-tooltip.component.spec.ts | 114 + .../help-tooltip/help-tooltip.component.ts | 165 +- .../shared/popup-coordinator.spec.ts | 186 + .../shared/popup-coordinator.ts | 257 + .../progress-stepper.component.html | 54 +- .../progress-stepper.component.scss | 162 +- .../progress-stepper.component.spec.ts | 97 + .../progress-stepper.component.ts | 27 + .../shared/projection-preview/engine/index.ts | 10 + .../engine/preview-draw-cluster.ts | 159 + .../engine/preview-draw-linear.ts | 189 + .../engine/preview-draw-manifold.ts | 109 + .../engine/preview-engine.spec.ts | 163 + .../engine/preview-geometry.ts | 329 + .../projection-preview/engine/preview-math.ts | 94 + .../engine/preview-primitives.ts | 188 + .../engine/preview-renderer.ts | 60 + .../engine/preview-types.ts | 153 + .../projection-preview.component.html | 66 +- .../projection-preview.component.scss | 77 +- .../projection-preview.component.spec.ts | 217 + .../projection-preview.component.ts | 1457 +--- .../projection-preview.data.ts | 189 + .../shared/step-summaries.spec.ts | 146 + .../shared/step-summaries.ts | 72 + .../shared/wizard-announcer.service.spec.ts | 26 + .../shared/wizard-announcer.service.ts | 21 + .../wizard-issue-card.component.html | 21 +- .../wizard-issue-card.component.scss | 95 +- .../wizard-issue-card.component.spec.ts | 51 +- .../wizard-issue-card.component.ts | 15 +- .../shared/wizard-step.ts | 8 +- .../step1-upload/step1-upload.component.html | 61 +- .../step1-upload/step1-upload.component.scss | 177 +- .../step1-upload.component.spec.ts | 168 + .../step1-upload/step1-upload.component.ts | 131 +- .../step2-column-selection.component.html | 55 +- .../step2-column-selection.component.scss | 104 +- .../step2-column-selection.component.spec.ts | 197 + .../step2-column-selection.component.ts | 78 +- .../consequence-hint.component.scss | 16 + .../consequence-hint.component.ts | 25 + .../consequence-preview.spec.ts | 339 + .../consequence-preview.ts | 265 + .../step3-accessibility.scss | 24 + .../step3-accessibility.spec.ts | 275 + ...ep3-configure-data-features.component.html | 270 +- ...ep3-configure-data-features.component.scss | 414 +- ...-configure-data-features.component.spec.ts | 301 + ...step3-configure-data-features.component.ts | 241 +- .../projection-methods.ts | 237 + .../step4-accessibility.spec.ts | 211 + .../step4-projection-settings.component.html | 336 + .../step4-projection-settings.component.scss | 630 ++ ...tep4-projection-settings.component.spec.ts | 468 ++ .../step4-projection-settings.component.ts | 392 + ...tep4-visualization-settings.component.html | 581 -- ...tep4-visualization-settings.component.scss | 1112 --- ...4-visualization-settings.component.spec.ts | 354 - .../step4-visualization-settings.component.ts | 1073 --- .../glyph-preview-geometry.spec.ts | 16 + .../glyph-preview-geometry.ts | 132 + .../step5-glyph-color-settings.component.html | 367 + .../step5-glyph-color-settings.component.scss | 490 ++ ...ep5-glyph-color-settings.component.spec.ts | 677 ++ .../step5-glyph-color-settings.component.ts | 519 ++ .../step5-review-processing.component.html | 395 - .../step5-review-processing.component.scss | 660 -- .../background-projections.spec.ts | 78 + .../background-projections.ts | 96 + .../step6-review-processing.component.html | 482 ++ .../step6-review-processing.component.scss | 555 ++ .../step6-review-processing.component.spec.ts | 542 ++ .../step6-review-processing.component.ts} | 517 +- .../testing/preprocessing-test-helpers.ts | 32 + src/app/services/data-processor.spec.ts | 138 + src/app/services/data-processor.ts | 44 +- .../dataset-collection.service.spec.ts | 19 +- src/app/services/filter.service.spec.ts | 26 +- src/app/services/projection.service.spec.ts | 303 + src/app/services/projection.service.ts | 336 +- src/app/services/toast.service.spec.ts | 172 +- src/app/services/toast.service.ts | 83 +- .../color-scale-selector.component.html | 79 +- .../color-scale-selector.component.scss | 22 + .../color-scale-selector.component.spec.ts | 142 + .../color-scale-selector.component.ts | 138 +- .../components/toast/toast.component.spec.ts | 121 + .../components/toast/toast.component.ts | 199 +- src/app/shared/helpers/color-helper.spec.ts | 44 +- src/app/shared/helpers/d3-helper.spec.ts | 87 + src/app/shared/helpers/d3-helper.ts | 47 +- src/app/shared/helpers/glyph-helper.ts | 2 +- src/app/shared/helpers/spatial-grid.spec.ts | 16 +- src/app/shared/helpers/three-helper.spec.ts | 119 + src/app/shared/helpers/three-helper.ts | 84 +- src/app/shared/interfaces/color-scale.ts | 5 +- src/app/shared/interfaces/pyodide-messages.ts | 12 +- src/app/shared/types/projection.types.ts | 11 - src/app/shared/utils/histogram.utils.spec.ts | 26 + src/app/shared/utils/histogram.utils.ts | 32 +- .../sidebar/histogram/histogram.component.ts | 61 +- src/app/sidebar/sidebar.component.html | 4 +- src/app/types/druidjs.d.ts | 83 - src/app/workers/druid-runner.spec.ts | 233 + src/app/workers/druid-runner.ts | 176 + src/app/workers/projection.worker.spec.ts | 116 + src/app/workers/projection.worker.ts | 399 +- src/app/workers/pyodide-calls.spec.ts | 132 + src/app/workers/pyodide-calls.ts | 110 + src/app/workers/pyodide-files.spec.ts | 91 + src/app/workers/pyodide-files.ts | 72 + src/app/workers/pyodide.worker.ts | 439 +- src/assets/preprocessing_processor.py | 120 +- src/assets/preprocessing_processor_config.py | 364 +- src/assets/processor.py | 477 -- src/assets/samples/wine-quality-red.csv | 1600 ++++ src/licenses/GPL-3.0.txt | 674 ++ src/licenses/LGPL-3.0.txt | 165 + src/licenses/README.txt | 19 + src/styles.scss | 23 + src/styles/_mixins.scss | 43 +- src/styles/_variables.scss | 144 +- src/test-setup.ts | 9 + src/testing/create-spy-obj.ts | 19 + tools/dataset_common.py | 454 ++ tools/make-sample-csv.py | 62 + tools/process.py | 561 +- tools/tests/test_preprocessing_config.py | 521 ++ tools/tests/test_process_cli.py | 265 + tsconfig.app.json | 4 +- tsconfig.spec.json | 3 +- 184 files changed, 27199 insertions(+), 14886 deletions(-) create mode 100644 .nvmrc create mode 100644 docs/wizard.md create mode 100644 src/app/canvas/tooltip/tooltip.component.spec.ts create mode 100644 src/app/menubar/menubar.component.spec.ts create mode 100644 src/app/preprocessing-wizard/preprocessing-wizard.dialog.spec.ts create mode 100644 src/app/preprocessing-wizard/services/column-defaults.spec.ts create mode 100644 src/app/preprocessing-wizard/services/column-defaults.ts create mode 100644 src/app/preprocessing-wizard/services/history-diff.ts create mode 100644 src/app/preprocessing-wizard/services/preprocessing.service.worker-files.spec.ts create mode 100644 src/app/preprocessing-wizard/services/processing-config.ts create mode 100644 src/app/preprocessing-wizard/services/wizard-storage.spec.ts create mode 100644 src/app/preprocessing-wizard/services/wizard-storage.ts create mode 100644 src/app/preprocessing-wizard/shared/constants/step-info.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/data-preview-table/data-preview-table.component.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/dialog-focus.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/dialog-focus.ts create mode 100644 src/app/preprocessing-wizard/shared/help-tooltip/help-tooltip.component.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/popup-coordinator.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/popup-coordinator.ts create mode 100644 src/app/preprocessing-wizard/shared/progress-stepper/progress-stepper.component.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/index.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-draw-cluster.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-draw-linear.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-draw-manifold.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-engine.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-geometry.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-math.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-primitives.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-renderer.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/engine/preview-types.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/projection-preview.component.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/projection-preview/projection-preview.data.ts create mode 100644 src/app/preprocessing-wizard/shared/step-summaries.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/step-summaries.ts create mode 100644 src/app/preprocessing-wizard/shared/wizard-announcer.service.spec.ts create mode 100644 src/app/preprocessing-wizard/shared/wizard-announcer.service.ts create mode 100644 src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step2-column-selection/step2-column-selection.component.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step3-configure-data-features/consequence-hint.component.scss create mode 100644 src/app/preprocessing-wizard/steps/step3-configure-data-features/consequence-hint.component.ts create mode 100644 src/app/preprocessing-wizard/steps/step3-configure-data-features/consequence-preview.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step3-configure-data-features/consequence-preview.ts create mode 100644 src/app/preprocessing-wizard/steps/step3-configure-data-features/step3-accessibility.scss create mode 100644 src/app/preprocessing-wizard/steps/step3-configure-data-features/step3-accessibility.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step3-configure-data-features/step3-configure-data-features.component.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step4-projection-settings/projection-methods.ts create mode 100644 src/app/preprocessing-wizard/steps/step4-projection-settings/step4-accessibility.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step4-projection-settings/step4-projection-settings.component.html create mode 100644 src/app/preprocessing-wizard/steps/step4-projection-settings/step4-projection-settings.component.scss create mode 100644 src/app/preprocessing-wizard/steps/step4-projection-settings/step4-projection-settings.component.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step4-projection-settings/step4-projection-settings.component.ts delete mode 100644 src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.html delete mode 100644 src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.scss delete mode 100644 src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.spec.ts delete mode 100644 src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.ts create mode 100644 src/app/preprocessing-wizard/steps/step5-glyph-color-settings/glyph-preview-geometry.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step5-glyph-color-settings/glyph-preview-geometry.ts create mode 100644 src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.html create mode 100644 src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.scss create mode 100644 src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.ts delete mode 100644 src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html delete mode 100644 src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss create mode 100644 src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.spec.ts create mode 100644 src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.ts create mode 100644 src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.html create mode 100644 src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.scss create mode 100644 src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.spec.ts rename src/app/preprocessing-wizard/steps/{step5-review-processing/step5-review-processing.component.ts => step6-review-processing/step6-review-processing.component.ts} (54%) create mode 100644 src/app/preprocessing-wizard/testing/preprocessing-test-helpers.ts create mode 100644 src/app/services/data-processor.spec.ts create mode 100644 src/app/services/projection.service.spec.ts create mode 100644 src/app/shared/components/color-scale-selector/color-scale-selector.component.spec.ts create mode 100644 src/app/shared/components/toast/toast.component.spec.ts create mode 100644 src/app/shared/helpers/d3-helper.spec.ts create mode 100644 src/app/shared/helpers/three-helper.spec.ts create mode 100644 src/app/shared/utils/histogram.utils.spec.ts delete mode 100644 src/app/types/druidjs.d.ts create mode 100644 src/app/workers/druid-runner.spec.ts create mode 100644 src/app/workers/druid-runner.ts create mode 100644 src/app/workers/projection.worker.spec.ts create mode 100644 src/app/workers/pyodide-calls.spec.ts create mode 100644 src/app/workers/pyodide-calls.ts create mode 100644 src/app/workers/pyodide-files.spec.ts create mode 100644 src/app/workers/pyodide-files.ts delete mode 100644 src/assets/processor.py create mode 100644 src/assets/samples/wine-quality-red.csv create mode 100644 src/licenses/GPL-3.0.txt create mode 100644 src/licenses/LGPL-3.0.txt create mode 100644 src/licenses/README.txt create mode 100644 src/test-setup.ts create mode 100644 src/testing/create-spy-obj.ts create mode 100644 tools/dataset_common.py create mode 100644 tools/make-sample-csv.py create mode 100644 tools/tests/test_preprocessing_config.py create mode 100644 tools/tests/test_process_cli.py diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index fd233ce..da79ca3 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,25 +1,51 @@ -name: Deploy Angular to GitHub Pages +name: CI and deploy to GitHub Pages on: push: branches: - - main # or master, depending on your default branch + - main + pull_request: + +# Least privilege by default; only the deploy job may push to gh-pages. +permissions: + contents: read jobs: - build-and-deploy: + verify: runs-on: ubuntu-latest + concurrency: + group: verify-${{ github.ref }} + cancel-in-progress: true steps: - name: ⬇️ Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v7 - name: 🟢 Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v7 with: - node-version: 25 + node-version-file: .nvmrc + cache: npm - name: 📦 Install dependencies - run: npm install + run: npm ci + + - name: 🔍 Lint + run: npm run lint + + # Vitest runs the unit tests in Playwright's headless Chromium (version pinned by package-lock). + # Not cached on purpose: Playwright advises against it, restoring takes about as long as the + # download, and --with-deps has to install the Linux system packages either way. + - name: 🎭 Install Playwright Chromium + run: npx playwright install --with-deps --only-shell chromium + + # The test command lives in package.json ("test:ci") so the runner can be swapped in one place. + - name: 🧪 Unit tests + run: npm run test:ci + env: + # The runner image points CHROME_BIN at its own Google Chrome, which the Angular test + # builder would launch instead of the pinned Playwright Chromium. + CHROME_BIN: '' - name: 🛠️ Build Angular app run: npm run build -- --base-href=/glyphspace/ --configuration production @@ -27,10 +53,41 @@ jobs: - name: 📄 Create .nojekyll file run: echo > dist/glyphspace/browser/.nojekyll + # The builder writes the licence list next to browser/, which is the only folder published. + - name: 📜 Publish third-party licences + run: cp dist/glyphspace/3rdpartylicenses.txt dist/glyphspace/browser/ + + - name: 📤 Upload site artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v7 + with: + name: site + path: dist/glyphspace/browser/ + include-hidden-files: true # keep .nojekyll + if-no-files-found: error + retention-days: 1 + + deploy: + needs: verify + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: gh-pages-deploy + cancel-in-progress: false + + steps: + - name: 📥 Download site artifact + uses: actions/download-artifact@v8 + with: + name: site + path: site + - name: 🚀 Deploy to gh-pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dist/glyphspace/browser/ + publish_dir: ./site publish_branch: gh-pages - enable_jekyll: false # this adds the .nojekyll file automatically + enable_jekyll: false # this adds the .nojekyll file automatically diff --git a/.gitignore b/.gitignore index 7ffe0d3..2a50513 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ /.sass-cache /connect.lock /coverage +# Vitest browser mode: screenshots of failed tests (next to the spec) and their attachment copies +__screenshots__/ +/.vitest-attachments /libpeerconnection.log npm-debug.log testem.log @@ -43,7 +46,8 @@ Thumbs.db # Miscellaneous /.angular/cache -/src/assets/__pycache__ +__pycache__/ +.pytest_cache/ /src/assets/tests /.claude /.angular diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/README.md b/README.md index 9006338..2863379 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,28 @@ Glyphspace is a complete rewrite of the [Glyphboard application](https://github. See the running demo on Github pages: [https://visual-engineers.org/glyphspace/](https://visual-engineers.org/glyphspace/). -## Usage +## Using the Wizard -... coming soon. +The preprocessing wizard guides you through six steps to prepare your data for visualization: + +1. **Upload Data** — Load your CSV file. +2. **Select Columns** — Choose columns to keep; ID and low-value fields can be deselected. +3. **Configure Data & Features** — Set encoding, scaling, missing-value and outlier handling per column. +4. **Projection** — Select columns for dimensionality reduction and pick projection methods (PCA, UMAP, t-SNE). +5. **Glyphs & Color** — Choose glyph features and color encoding for the dashboard's initial view (both changeable later). +6. **Review & Process** — Review your configuration and process the data. + +**Undo/Redo:** Use the buttons in the wizard's title bar or Ctrl+Z / Ctrl+Shift+Z (Cmd on Mac) to step back and forth through your changes. + +**Try with sample data:** Step 1 offers a Wine Quality (red) dataset as a starter (P. Cortez et al., UCI Machine Learning Repository, CC BY 4.0). + +Developer notes on the wizard's architecture are in [docs/wizard.md](docs/wizard.md). ## Data Import ### Dynamic Import -Inside of the running application, you can upload suitable CSV files into the filesystem of Micropython running as a web assembly behind the scenes (see below for CSV file structure). You can also upload ZIP folders with thumbnails for your data. The filename must be the same as your CSV file and the contained files should be named according to the IDs of the data rows. +Inside of the running application, you can upload suitable CSV files into the filesystem of Pyodide (Python compiled to WebAssembly, loaded from the jsDelivr CDN; the version is set in `src/app/workers/pyodide.worker.ts`; see below for CSV file structure). You can also upload ZIP folders with thumbnails for your data. The filename must be the same as your CSV file and the contained files should be named according to the IDs of the data rows. ### Preprocessed Data @@ -26,7 +39,7 @@ You can either process CSV files via the command line using: python tools/process.py ``` -Which creates the necessary data files inside the `src/assets/data` folder of the project and recreates the file `src/defauult-dataset.ts` which the application picks up after starting. You can also store thumbnails in the folder `src/assets/data/thumbnails/`, which are in JPG format and have the ID of your data items as filename. +Which creates the necessary data files inside the `src/assets/data` folder of the project and recreates the file `src/default-dataset.ts` which the application picks up after starting. You can also store thumbnails in the folder `src/assets/data/thumbnails/`, which are in JPG format and have the ID of your data items as filename. All columns except IDs and positional information are converted with a label encoder if they are strings and then normalized for processing of the meta data (including a histogram, min, max, etc.) as well as a PCA, UMAP, and tSNE multidimensional reduction. @@ -55,65 +68,42 @@ Notable changes: - in feature.json `default-context` is renamed to `defaultcontext` for the same reasons - IDs are treated as strings -## Angular Development Instructions - -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.13. - -### Development server - -To start a local development server, run: - -```bash -ng serve -``` - -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. - -### Code scaffolding +## Development -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: +**Node version:** v24 (see `.nvmrc`) -```bash -ng generate component component-name -``` - -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: - -```bash -ng generate --help -``` - -### Building - -To build the project run: +### Setup ```bash -ng build +npm ci # Install dependencies (prefer ci over install for reproducible builds) +npx playwright install chromium # Once per machine: browser for the unit tests +npm start # Start local dev server at http://localhost:4200/ +npm test # Run unit tests (Vitest in headless Chromium, watch mode) +npm run test:ci # Run unit tests once (CI mode) +npm run lint # Run ESLint +npm run build # Build for production (output in dist/) ``` -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. - -### Running unit tests +### Python tools -To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: +The project includes Python preprocessing tools that run locally via `tools/process.py` (CLI; the dataset file generation lives in `tools/dataset_common.py`, the in-browser import is the wizard's pipeline in `src/assets/preprocessing_processor*.py`): ```bash -ng test +python tools/process.py # Process a CSV and generate src/default-dataset.ts +python tools/process.py # Rebuild default dataset list +python -B -m pytest -p no:cacheprovider tools/tests # Run Python tests ``` -### Running end-to-end tests +Other tools: +- `tools/make-sample-csv.py` — Generate sample data -For end-to-end (e2e) testing, run: - -```bash -ng e2e -``` +### Deployment -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. +The app is deployed to GitHub Pages on every push to `main`. See `.github/workflows/deploy.yaml` for the workflow (runs tests, lints, builds on PRs; publishes on merge to main). -### Additional Resources +## Licence -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +GlyphSpace is licensed under the MIT License (see [LICENSE](LICENSE)). It bundles [DruidJS](https://github.com/saehm/DruidJS) for the dimensionality reductions, which is licensed under the LGPL-3.0-or-later. The deployed site ships the LGPL and GPL texts and a notice in `licenses/` (source: `src/licenses/`), plus `3rdpartylicenses.txt` listing all bundled packages. diff --git a/ROADMAP.md b/ROADMAP.md index a2a7510..fd93f60 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -396,9 +396,29 @@ Eigenständiges Folge-Arbeitspaket zu A2, umgesetzt und gemergt (7 neue Unit-Tes --- +## Review-Runde (September 2026) + +Ergebnis eines Code-, UI- und Barrierefreiheits-Reviews des Integrations-Branches. Details für Entwickler: `docs/wizard.md`. + +- **Korrektheit / Undo** — Jede Nutzeraktion ist genau ein Undo-Schritt: Schritt 3 füllte die Historie beim Öffnen (Ausreißerzählung), „Reset to Default“/„Apply to same type“ und die Master-Checkbox erzeugten mehrere Einträge, Projektionsänderungen umgingen Historie und Speicherung. Toast-„Undo“ macht nur noch die eigene Aktion rückgängig (`undoIfLatest`). Kein Undo während der Verarbeitung (inkl. FastMap). Schließen während der Schritt-2→3-Animation, doppelter Start in Schritt 6 und der Shift-Klick-Bereich nach Filterwechsel korrigiert. +- **Sicherheit** — Python-Code-Injection über Spaltennamen im CSV-Header bzw. Dateinamen behoben: Werte gehen nur noch als Daten an Pyodide (`workers/pyodide-calls.ts`). Dateinamen wie `pandas.py` oder `../` werden abgelehnt; ein Zip namens `lib.zip` kann `/lib` nicht mehr löschen. Worker-Anfragen haben Request-IDs. Stored XSS im Dashboard-Tooltip behoben: CSV-Werte, Spaltennamen und IDs wurden per `bypassSecurityTrustHtml` als HTML eingefügt; der Tooltip rendert sie jetzt als Text. +- **Pipeline-Optionen jetzt wirksam** — Wirkungslos waren bisher: Fehlende Werte „Remove rows“ je Spalte, Ausreißer „Remove“/„Cap“, Encodings „Normalize“/„Standardize“, „Remove duplicates“ bei CSVs ohne ID-Spalte, UMAP-Mindestabstand 0. Außerdem behoben: Absturz bei Label-Encoding nach Zeilenlöschung, verrutschte Tooltip-Werte, String-Füllwert in Zahlenspalten, Ausreißer bei Boolean-Spalten, vertauschte „Breitengrad“/„Längengrad“. Neue Fehlerklasse K8 (Füllwert passt nicht). 140 Python-Tests (`tools/tests/`). +- **Fehlende Werte / Datenqualität** — Tooltips zeigen „missing“ statt „0“ (bzw. „nan“ aus der CLI). Fehlende Zahlen und Datumswerte werden erst nach der Skalierung auf 0 gesetzt und verzerren die echten Werte nicht mehr (vorher z. B. alle Datumswerte in ~4 % des Bereichs, fehlende Daten als 1970-01-01). Boolean-Beschriftungen waren vertauscht, wenn die erste Zeile „True“ war. Die Auswahlfelder in Schritt 3 zeigten „None“/„Keep“, obwohl eine andere Einstellung aktiv war. CLI (`tools/process.py`): Beschriftungen nach One-Hot-Spalten verschoben, Tooltips für Kategorien mit Unterstrich, Kategorien bei Label-Encoding mit fehlenden Werten — behoben. +- **Schritt-Aufteilung** — Schritt 4 „Projection“ und Schritt 5 „Glyphs & Color“ statt eines gemeinsamen Visualisierungsschritts; Review ist Schritt 6. Schritte über Namen statt Nummern (`shared/constants/step-info.ts`), gespeicherte Zustände werden migriert. +- **Wizard-UI** — Undo/Redo in der Titelleiste neben „Start over“ (Icon `restart_alt`, Bestätigung als nativer Dialog). Schritt 3 zeigt die Folgen einer Einstellung mit echten Zahlen der Daten; Schritt 1 bietet Beispieldaten (Wine Quality, CC BY 4.0); die Schrittleiste fasst getroffene Entscheidungen zusammen. Laufende Hintergrund-Projektionen lassen sich einzeln abbrechen und neu starten. +- **Barrierefreiheit** — Wizard als modaler Dialog (Fokusfalle, Escape, Fokus-Rückgabe), Fokus und Ansage bei Schrittwechsel, Datei-Upload per Tastatur, Beschriftungen für alle Bedienelemente, Tastatur-Alternative zum Drag-and-drop der Glyph-Merkmale, Live-Regions für Status und Toasts, Tooltips per Fokus öffnen und per Escape schließen, Fortschrittsbalken und Fehlermeldungen für Screenreader. +- **Design / Größen** — Eine Typo-Skala (11–24 px, keine Schrift unter 11 px), Abstands-, Icon- und z-index-Tokens, einheitliche Inhaltsbreite in allen Schritten, gemeinsame Farbtokens für Fehler/Warnung/Erfolg statt ~40 einzelner Hex-Werte. Alle Text- und Fokusfarben erfüllen WCAG-Kontrast (Akzent `#00737d` statt `#00bcd4` für Text, Fokusring und Primär-Buttons). Schritte 3–5 stapeln sich unter 1024 px, nichts wird bei 200 % Zoom abgeschnitten; `prefers-reduced-motion` wird im ganzen Wizard beachtet. +- **Abhängigkeiten / CI / Bundle** — Angular 21.2.24 (Sicherheits-Patches), three 0.186, zone.js 0.16, Pyodide 0.29.5; `gsap` durch die Web Animations API ersetzt; ungenutzte Pakete entfernt; `npm audit` ohne Befund. Wizard wird per `@defer` nachgeladen (Initial-Bundle 1,47 → 1,12 MB), nur noch die genutzte Icon-Schriftart. druidjs 0.9 (Projektionen insgesamt ~2,7× schneller, korrigiertes UMAP/SAMMON; Lizenz jetzt LGPL-3.0-or-later — Lizenztexte werden unter `licenses/` mit ausgeliefert, `3rdpartylicenses.txt` wird veröffentlicht). Tests: Karma + Jasmine durch Vitest ersetzt (Browser-Modus, headless Chromium über Playwright). CI: Node 24 (`.nvmrc`), `npm ci`, Lint + Tests + Build vor dem Deploy. +- **Aufräumen** — `projection-preview` von 1.397 auf 186 Zeilen (Engine ausgelagert), Projektions-Worker von ~420 auf 62 Zeilen, toter In-Browser-CSV-Pfad (`processor.py`, Worker-Nachricht `process`) entfernt, `tools/dataset_common.py` nur noch für die lokale CLI, toter Code entfernt, Texte durchgehend Englisch. + +--- + ## Ausblick — offen, aber nicht im Umsetzungsrahmen Identifizierte, aber bewusst nicht umgesetzte Punkte: - **Methodenspezifische Standardwerte je Projektionsmethode** — laut Anforderungskatalog bewusst außerhalb des Rahmens. -- **Manuelles Beenden laufender Reduktionsmethoden** — aktuell lässt sich eine hängende/langlaufende Dimensionsreduktion nur über „Start over" stoppen, was den gesamten Wizard zurücksetzt (Konfiguration geht verloren). Ein gezielter Abbruch je Methode (bzw. globales „Verarbeitung abbrechen" ohne Reset) sowie ein Timeout pro Methode fehlen. Technische Grundlage vorhanden (isolierte Web-Worker, `ProjectionService.terminateAllWorkers()`), aber ohne nutzerseitige Steuerung. Offen für eine spätere Erweiterung. +- **Manuelles Beenden laufender Reduktionsmethoden** — umgesetzt für Hintergrund-Projektionen (Abbrechen/Neu starten je Methode, „Stop all“; `ProjectionService.cancel/retry/terminateAllWorkers`). Der primäre Pyodide-Lauf bleibt nicht abbrechbar: den Worker zu beenden würde die geladene Python-Laufzeit und die hochgeladene Datei verwerfen. Ein Timeout je Methode fehlt weiterhin. +- **Fehlende Werte bei Skalierung „None“** — landen bei 0 in Originaleinheiten (z. B. Breitengrad 0°, Datum 1970); betrifft nur Spalten ohne Skalierung (Koordinaten standardmäßig). +- **Upgrades** — Angular 22 (Komponenten standardmäßig OnPush; eigener PR, Angular 21 erhält Sicherheitsupdates bis ca. Mai 2027), Pyodide 314 (pandas 3; kein sichtbarer Nutzen, größerer Download; die Pipeline ist dafür vorbereitet und getestet). Erledigt: druidjs 0.9 (ändert die Einbettungen; TriMaps Parameter `weight_adj` gibt es dort nicht mehr, der Regler „Weight Adjustment“ wurde daher entfernt — TriMap läuft mit den Standardwerten der Bibliothek). +- **LLE vorübergehend ausgeblendet** — druidjs 0.9 liefert falsche LLE-Einbettungen (Eigenvektor-Shift ≈ 0, Power-Iteration findet die größten statt der kleinsten Eigenvektoren); wieder einblenden, sobald upstream behoben (`enableLLE` aus `HIDDEN_PROJECTION_METHODS` in `projection-methods.ts` entfernen). diff --git a/angular.json b/angular.json index e758743..1665789 100644 --- a/angular.json +++ b/angular.json @@ -27,10 +27,11 @@ "inlineStyleLanguage": "scss", "assets": [ "src/assets", - "src/favicon.ico" + "src/favicon.ico", + { "glob": "*.txt", "input": "src/licenses", "output": "licenses" } ], "styles": [ - "node_modules/material-icons/iconfont/material-icons.css", + "node_modules/material-icons/iconfont/filled.css", "src/styles.scss" ], "scripts": [], @@ -41,8 +42,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "1MB", - "maximumError": "2MB" + "maximumWarning": "1.25MB", + "maximumError": "1.7MB" }, { "type": "anyComponentStyle", @@ -80,25 +81,21 @@ "builder": "@angular/build:extract-i18n" }, "test": { - "builder": "@angular/build:karma", + "builder": "@angular/build:unit-test", "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], + "runner": "vitest", "tsConfig": "tsconfig.spec.json", - "inlineStyleLanguage": "scss", - "assets": [ - { - "glob": "**/*", - "input": "public" - } + "browsers": [ + "ChromiumHeadless" ], - "styles": [ - "src/styles.scss" + "browserViewport": "1280x720", + "setupFiles": [ + "src/test-setup.ts" ], - "scripts": [], - "webWorkerTsConfig": "tsconfig.worker.json" + "coverageReporters": [ + "html", + "text-summary" + ] } }, "lint": { diff --git a/docs/wizard.md b/docs/wizard.md new file mode 100644 index 0000000..2f88e0d --- /dev/null +++ b/docs/wizard.md @@ -0,0 +1,80 @@ +# Preprocessing wizard — developer notes + +The wizard turns an uploaded CSV into a dataset for the dashboard. It runs entirely in the browser: Python (pandas, scikit-learn) in a Pyodide web worker, dimensionality reductions (DruidJS) in further workers. All paths below are relative to `src/app/` unless they start with `src/` or `tools/`. + +## Steps + +The ordered step list is `WIZARD_STEPS` in `preprocessing-wizard/shared/constants/step-info.ts`. Code refers to steps by id, never by number; helpers `stepIndex`, `stepIdAt`, `stepNumber`, `stepLabel` convert. + +| # | id | Title | +|---|----|-------| +| 1 | `upload` | Upload Data | +| 2 | `columns` | Select Columns | +| 3 | `features` | Configure Data & Features | +| 4 | `projection` | Projection | +| 5 | `appearance` | Glyphs & Color | +| 6 | `review` | Review & Process | + +Steps 1–5 implement `WizardStep` (`shared/wizard-step.ts`) so the shell renders one navigation bar. Glyph and colour settings (step 5) only set the dashboard's initial view; the dashboard's Encoding panel can change them later. + +## Shell (`preprocessing-wizard/preprocessing-wizard.component.ts`) + +- **Dialog:** the root is `role="dialog"` / `aria-modal="true"`, labelled by the h1 `#wizard-dialog-title`. Focus moves to it on open, Tab is wrapped inside (`shared/dialog-focus.ts`), Escape closes the wizard unless a popup or the Start-over `` is open, and focus returns to the opener in the menubar. The wizard chunk is lazy-loaded with `@defer` from `menubar/menubar.component.html`. +- **Step changes:** focus moves to the "Step N of 6" heading and the change is announced. +- **Announcements:** `shared/wizard-announcer.service.ts` (`WizardAnnouncer.announce(message, 'polite' | 'assertive')`). The shell renders the two always-present live regions; steps never create their own. +- **Deep links:** `PreprocessingService.goToStepWithScroll(id, anchorId)` (history hint "Show change", Review edit buttons, issue-card "Fix in step N"). The shell takes the anchor with `consumeScrollTarget()`, then scrolls to and focuses it after render. Anchors are `wizard-anchor-*` ids in the step templates. +- **Stepper:** `shared/progress-stepper/` — `
    ` of buttons with `aria-current="step"`; completed steps show a summary from `shared/step-summaries.ts`. +- **Title bar:** Undo, Redo (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y; ignored in text fields and while processing), Start over (native `` confirmation), Close. +- **Step 2 → 3 transition:** Web Animations API, skipped under `prefers-reduced-motion`, cancelled on destroy. + +## State and persistence + +`preprocessing-wizard/services/preprocessing.service.ts` owns `PreprocessingState` (`models/preprocessing-state.ts`). State is saved to localStorage by `services/wizard-storage.ts` with a format version (currently 2); older saves are migrated by step id (the old 5-step layout's review step 4 becomes 5), and projection settings that no longer exist (e.g. `trimapWeightAdj`) are dropped. Column defaults and the "smart defaults" live in `services/column-defaults.ts`; the config sent to Python is built in `services/processing-config.ts`. + +## Undo / redo + +- Every user action pushes exactly one snapshot (`pushHistory(label)`, capped at 50). Bulk actions use `updateColumnConfigs(changes, label)`; derived metadata such as outlier counts uses `updateColumnMetadata` and never creates an entry. +- `services/history-diff.ts` diffs snapshots and maps the changed field to a label, step id and anchor (`SETTING_META`) for the undo hint. +- Toast "Undo" buttons call `undoIfLatest(id)`, so they only revert their own action. +- While processing, undo/redo are no-ops (`holdProcessing()` / `whileProcessing(task)` keep `isProcessing` up until the whole primary run, FastMap included, finishes). +- The worker keeps uploaded CSVs for reprocessing; the service calls `DataProcessorService.pruneFiles(keep)` with every file name still referenced by the state or the undo/redo stacks. + +## Errors + +`preprocessing-wizard/shared/constants/wizard-error-classes.ts` maps raw worker errors to classes with a cause, a fix and a target step: + +| Class | Meaning | Fix in | +|---|---|---| +| K1 | Text values reached the numeric feature matrix | features | +| K2 | Cleaning removed every row | features | +| K3 | A column is constant or almost empty | columns | +| K4 | A neighbour parameter is ≥ the number of rows | projection | +| K5 | Glyph feature count outside 3–12 | appearance | +| K6 | Time or memory limit | projection | +| K7 | A text column has too many distinct values to one-hot encode | columns | +| K8 | A fill value doesn't fit its numeric column | features | + +Unmatched errors fall back to a generic blocking issue. `detectPreflightIssues` warns before processing (K3, K4, K6). Failed background projections become dismissable warnings (`classifyBackgroundFailure`). + +## Processing pipeline + +- **Worker:** `workers/pyodide.worker.ts` loads Pyodide from the jsDelivr CDN (`PYODIDE_VERSION`), installs pandas and scikit-learn, and writes the Python modules from `src/assets/` into its file system on demand. Requests carry a `requestId` and are handled one at a time. +- **No Python built from data:** `workers/pyodide-calls.ts` only turns fixed module/function names into code; all arguments (file names, column names from the CSV header, the config) are passed as data in a throwaway namespace. +- **File names:** `workers/pyodide-files.ts` rejects path separators, `.`/`..`, and importable suffixes (`.py`, `.pyc`, `.so`), and tracks the files the worker wrote for `pruneFiles`. +- **Order of operations** (header of `src/assets/preprocessing_processor_config.py`): fill missing values → cap outliers (detected on the original values) → drop rows (`remove_rows` columns, outlier `remove`) → drop duplicates → encode (one-hot, label, normalize, standardize, dates, booleans) → scale. Outlier detection is shared with Step 3's count (`find_outliers` in `src/assets/preprocessing_processor.py`). +- **Projections:** DruidJS (0.9: WASM kernels with a JS fallback; its worker pool is Node-only, so browsers get no nested workers) runs in `workers/projection.worker.ts` via `workers/druid-runner.ts`; only the primary FastMap runs on the main thread, loading DruidJS on first use. `services/projection.service.ts` runs each background method in its own worker and offers `cancel(method)`, `retry(method)`, `terminateAllWorkers()` and `detachAll()`. The primary Pyodide run can't be cancelled: terminating that worker would drop the loaded runtime and the uploaded file. +- **Hidden methods:** methods in `HIDDEN_PROJECTION_METHODS` (`preprocessing-wizard/steps/step4-projection-settings/projection-methods.ts`) are not offered in Step 4 and never run, summarised or warned about, even if a saved config enables them. LLE is hidden until druidjs fixes it (see ROADMAP); TriMap has no parameters and runs with DruidJS's defaults. +- **Local CSV import:** the CLI `tools/process.py` (with `tools/dataset_common.py`) writes the bundled datasets in `src/assets/data/`. It is not part of the app build; in the browser, CSV files are only imported through this wizard pipeline. + +## UI helpers + +- **Popups:** help tooltips and method previews go through `shared/popup-coordinator.ts` — one popup at a time, opens on hover/focus/click, closes on Escape or outside click, 180 ms hover grace. Method previews (`shared/projection-preview/`) draw synthetic data with a framework-free engine in `engine/`. +- **Consequence hints (Step 3):** `steps/step3-configure-data-features/consequence-preview.ts` turns the column profile into text such as "12.4 – 98.1 → 0.00 – 1.00" or "Drops 132 of 5,000 rows". +- **Styles:** tokens in `src/styles/_variables.scss`, wizard mixins in `preprocessing-wizard/shared/_wizard-shared.scss`. + +## Tests + +- Specs sit next to the code (`*.spec.ts`) and run with Vitest in headless Chromium (Angular `@angular/build:unit-test`, Playwright provider): `npm test` watches, `npm run test:ci` runs once. One file: `npx ng test --include=`. +- Shared test helpers: `preprocessing-wizard/testing/preprocessing-test-helpers.ts` (`createTestPreprocessingService()`, `createToastServiceSpy()`) and `src/testing/create-spy-obj.ts`. `src/test-setup.ts` restores `vi.spyOn` spies after every test. +- Python pipeline: `python -B -m pytest -p no:cacheprovider tools/tests/test_preprocessing_config.py` (each test runs with default pandas and with pandas-3 behaviour enabled; the wizard's option values are read from the TypeScript enums). +- Sample data for manual testing: "Try with sample data" in Step 1 (`src/assets/samples/wine-quality-red.csv`, regenerated by `tools/make-sample-csv.py`). diff --git a/package-lock.json b/package-lock.json index b762b2d..6824db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,47 +8,40 @@ "name": "glyphspace", "version": "0.0.0", "dependencies": { - "@angular/common": "^21.1.4", - "@angular/compiler": "^21.1.4", - "@angular/core": "^21.1.4", - "@angular/forms": "^21.1.4", - "@angular/platform-browser": "^21.1.4", - "@angular/platform-browser-dynamic": "^21.1.4", - "@saehrimnir/druidjs": "^0.7.3", + "@angular/common": "^21.2.24", + "@angular/compiler": "^21.2.24", + "@angular/core": "^21.2.24", + "@angular/forms": "^21.2.24", + "@angular/platform-browser": "^21.2.24", + "@saehrimnir/druidjs": "^0.9.0", "d3": "^7.9.0", - "d3-force": "^3.0.0", - "gsap": "^3.15.0", "material-icons": "^1.13.14", - "pyodide": "^0.28.0", "rxjs": "~7.8.0", - "three": "^0.176.0", + "three": "^0.186.0", "tslib": "^2.3.0", - "zone.js": "~0.15.0" + "zone.js": "~0.16.3" }, "devDependencies": { - "@angular/build": "^21.1.4", - "@angular/cli": "^21.1.4", - "@angular/compiler-cli": "^21.1.4", + "@angular/build": "^21.2.24", + "@angular/cli": "^21.2.24", + "@angular/compiler-cli": "^21.2.24", "@types/d3": "^7.4.3", - "@types/d3-force": "^3.0.10", - "@types/d3-quadtree": "^3.0.6", - "@types/jasmine": "~5.1.0", - "@types/node": "^18.18.0", - "@types/three": "^0.176.0", - "angular-eslint": "21.0.1", - "eslint": "^9.39.1", + "@types/three": "^0.186.0", + "@vitest/browser-playwright": "^4.1.11", + "@vitest/coverage-v8": "^4.1.11", + "angular-eslint": "~21.4.0", + "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", - "jasmine-core": "~5.6.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.1.0", "lint-staged": "^16.2.7", + "playwright": "^1.62.1", "prettier": "^3.8.1", "typescript": "~5.9.3", - "typescript-eslint": "8.46.4" + "typescript-eslint": "^8.70.1", + "vitest": "^4.1.11" + }, + "engines": { + "node": "^24.15.0 || >=26.0.0" } }, "node_modules/@algolia/abtesting": { @@ -275,13 +268,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2102.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.11.tgz", - "integrity": "sha512-t7J8aaUho1mXjiIecPNX5/rjXeV8j8ZCGY5tD3ic5kzKxPkbuYYcQpJLdzlmBcN+wDgCmNdo8ySvItvU0m58lg==", + "version": "0.2102.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.24.tgz", + "integrity": "sha512-U/d5l/1rfrNSlbGjMTunyzwxh+2vJce3y0r0tIx/NOFZbnYhq+gx+e33mR6oxI4yk/1Jks67ltvstrJuBbVbYg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.11", + "@angular-devkit/core": "21.2.24", "rxjs": "7.8.2" }, "bin": { @@ -294,9 +287,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "21.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.11.tgz", - "integrity": "sha512-kfMNh5X2hOdyr0uNFaaHUJR3OVr4oH2+UhI+FsTu7gqogdgYlHAVHhHAFulfDgtAEOiqpeSQF9RhQnCJl+/LXA==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.24.tgz", + "integrity": "sha512-QLYLd6OcfQS5qWTlzG1/d8lFTcJDFcsY6sOaGVtrruYiDxrbeM2iJttoIoKzvbNPROFyo3fANVQJOVhytpXWPg==", "dev": true, "license": "MIT", "dependencies": { @@ -322,13 +315,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.11.tgz", - "integrity": "sha512-69CWZ5/ftLdpUPAwwdAxTNosiGXUyvwdnOfmHsd9NvCT0OSTeq0eQ0UfnGcHASrXIVmnyWiNfBWM1DLqsgBXmw==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.24.tgz", + "integrity": "sha512-mLOme2vfbLrHFmMyM6ybPuGCnCvPiemXWdHnptY2Q7Hzum72DabfgtTNsrMcI2F5zrQV2DDQunNjC5cXqtq4/w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.11", + "@angular-devkit/core": "21.2.24", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.3.0", @@ -341,9 +334,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.0.1.tgz", - "integrity": "sha512-6BqpmW0XvjTOs2YOHwzeZcQ32eL8vs8SCHjt1cQnq1+libOVDXky1eb/jRs7ouyA49UagLDoM34K1kjrYo8P3Q==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.4.0.tgz", + "integrity": "sha512-3kgGmrVaCYbLtDjC8g4BmMBbdz4thsOB8/NYly8JtXM8EuDZEk5Pz6VTRpJR02ARprwayraTTmhyvq6OGBlQ9w==", "dev": true, "license": "MIT", "dependencies": { @@ -351,130 +344,121 @@ "@angular-devkit/core": ">= 21.0.0 < 22.0.0" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.0.1.tgz", - "integrity": "sha512-Kb59SopkJ2sDgZSysL+ZqcfqM2cbK+gciAyHljkrCUsqo66eEq5KCZUU//RVoo4MHi+qL/dFy54JG/+A/35xcQ==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.4.0.tgz", + "integrity": "sha512-/3H4BPbQ1BHJkkrUsfusZtmHc+qiFWBBZ9UDPWah4xZMjflexOK9U4GYeH7nMjcuyqFnIlMMeJJNwNLGt/hmdg==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.0.1.tgz", - "integrity": "sha512-tSb5qgIwoMrX3Z17dSsHrNFWrgBWafxK7IQudU0RXxdzq6joq1qDrzHwLT3Jn+Y6ocn0jdavAefEGHAhomCjcQ==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.4.0.tgz", + "integrity": "sha512-mow2DMj+xBvGl5t7jzC34R8YfbHbaGNyCNFzpovtl9qc0JbuqLyg6htmt8xb05f8ZjATOr4nz0ESt6HV4c51hw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.1", - "@angular-eslint/utils": "21.0.1", + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "@angular-eslint/utils": "21.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.0.1.tgz", - "integrity": "sha512-DF1WEMalbV1hNKxbu3nwK1yUa+E2FQpNz0KDORU65/vdCffeuftCetobrsAS7zDgJ6FO+Fsb+ZeCzNKEhhh1vA==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.4.0.tgz", + "integrity": "sha512-sJEHx2WYnvOgPpzP1eHnUdRS06zgKmRxbiIR0JiCcaSen5iv1HlsMieXy//FS9TtNW+abHOy4UtDuGuSPflPFA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.1", - "@angular-eslint/utils": "21.0.1", + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "@angular-eslint/utils": "21.4.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { - "@angular-eslint/template-parser": "21.0.1", + "@angular-eslint/template-parser": "21.4.0", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/schematics": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.0.1.tgz", - "integrity": "sha512-IdtGdRPuJctHuiZ8v8SN3MqWiUa3cD9Q5jFvIRkAkjpHXXmTk5PYelSjUP8UX/zfUfFkxHXghasTmJd2+252OQ==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.4.0.tgz", + "integrity": "sha512-crD6Hfxs7x5bN9FCqTZI7uVSiGvprfCS3MCPOpyIQl87bRr/9aNhnicJ3ROUHv+2A713BgPHIgiCII/bxzrfPw==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.0.1", - "@angular-eslint/eslint-plugin-template": "21.0.1", + "@angular-eslint/eslint-plugin": "21.4.0", + "@angular-eslint/eslint-plugin-template": "21.4.0", "ignore": "7.0.5", - "semver": "7.7.3", + "semver": "7.7.4", "strip-json-comments": "3.1.1" - } - }, - "node_modules/@angular-eslint/schematics/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0" } }, "node_modules/@angular-eslint/template-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.0.1.tgz", - "integrity": "sha512-1KocmjmBP0qlKQGRhRGN0MGvLxf1q2KDWbvzn7ZGdQrIDLC/hFJ8YmnOWsPrM9RxiZi0o5BxCCu9D7KlbthxIg==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.4.0.tgz", + "integrity": "sha512-BaUSLSyS+43fzDoJkTMkGqNdCXq3fGnUZsfXTmrlZPJf5AYFbgAlAPGZXDJyoNWw43fux+DafdlrlKcYUSgSIw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.1", - "eslint-scope": "^9.0.0" + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "eslint-scope": "9.1.2" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/utils": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.0.1.tgz", - "integrity": "sha512-tovWIDiEsfSAsPWH+/wL9Hfl/Hc+2j2IP+Z85I6uWTbynLVdyURx8gmJjKBUTSCmcyrgBnTbnnlr4DTM6/aFOg==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.4.0.tgz", + "integrity": "sha512-7pi+Ga7QmdH5Ig/diau6fR5L4yubgKr9TOjdCg7OeuE/zo0O3osTCNT6JOodzS/iQM1kSCJFDoIBKFeUOttiNw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.1" + "@angular-eslint/bundled-angular-compiler": "21.4.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular/build": { - "version": "21.2.11", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.11.tgz", - "integrity": "sha512-2afR6VKkP0HH2u6OuijSMgSHsL5tU4CBCixgQtY677mlvS8TOZg/kOksJIUlz0EvDVCJZBK8WLH9cPJ6mC/Qdg==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.24.tgz", + "integrity": "sha512-NurM2W7Q/lCGonkYgSNNxGrh9AYH3LksekYRVxl0KxVKa8Z5x6oMYlLfyI4Lv9Q4C1/x1SXv3xNxgYu/nIW4vA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.11", - "@babel/core": "7.29.0", + "@angular-devkit/architect": "0.2102.24", + "@babel/core": "7.29.7", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", "@inquirer/confirm": "5.1.21", "@vitejs/plugin-basic-ssl": "2.1.4", "beasties": "0.4.1", "browserslist": "^4.26.0", - "esbuild": "0.27.3", + "esbuild": "0.28.1", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", @@ -483,14 +467,14 @@ "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.4", - "piscina": "5.1.4", + "piscina": "5.2.0", "rolldown": "1.0.0-rc.4", "sass": "1.97.3", "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.15", - "undici": "7.24.4", - "vite": "7.3.2", + "undici": "7.29.1", + "vite": "7.3.6", "watchpack": "2.5.1" }, "engines": { @@ -509,7 +493,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.11", + "@angular/ssr": "^21.2.24", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -558,761 +542,1040 @@ } } }, - "node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", - "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0" + "node": ">=18" } }, - "node_modules/@angular/build/node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "node_modules/@angular/build/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@angular/cli": { - "version": "21.2.11", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.11.tgz", - "integrity": "sha512-vpF/oa+HzLl4lF78ePCgkhBdQj29IlFvZtBsbAXXpb16FLZSua2m7+yHd/PICTlchh1+LfIxFY9snMY1BllBsQ==", + "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.2102.11", - "@angular-devkit/core": "21.2.11", - "@angular-devkit/schematics": "21.2.11", - "@inquirer/prompts": "7.10.1", - "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.11", - "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.48.1", - "ini": "6.0.0", - "jsonc-parser": "3.3.1", - "listr2": "9.0.5", - "npm-package-arg": "13.0.2", - "pacote": "21.3.1", - "parse5-html-rewriting-stream": "8.0.0", - "semver": "7.7.4", - "yargs": "18.0.0", - "zod": "4.3.6" - }, - "bin": { - "ng": "bin/ng.js" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">=18" } }, - "node_modules/@angular/common": { - "version": "21.2.13", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.13.tgz", - "integrity": "sha512-fNvRmGAX0zbsLX/kJjgb6l8HAuGTpfYRNc06taTCIvED2RsRpfwrh79IxYlPBspr+hpFbHa0/kxU6Q5I8V0jKQ==", + "node_modules/@angular/build/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/core": "21.2.13", - "rxjs": "^6.5.3 || ^7.4.0" + "node": ">=18" } }, - "node_modules/@angular/compiler": { - "version": "21.2.13", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.13.tgz", - "integrity": "sha512-0OZk5ujHgowRme3iXJ1Ce1OI3eTDcGovBARBiyJT0E8kt9Y0TdQdGaYMRrNN1UzDv4hk8f1d/xVeF0BpMTvqPQ==", + "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/@angular/compiler-cli": { - "version": "21.2.13", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.13.tgz", - "integrity": "sha512-ueETJy2ZcXZ4a0aLEr+oPMw26f8Hn903WC4QN0MCH+sLB9Zustpzydqtmzo5mdSzwuoLoxcesYJTZFmpwD1xIQ==", + "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/core": "7.29.0", - "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^5.0.0", - "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.2.0", - "semver": "^7.0.0", - "tslib": "^2.3.0", - "yargs": "^18.0.0" - }, - "bin": { - "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/compiler": "21.2.13", - "typescript": ">=5.9 <6.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@angular/core": { - "version": "21.2.13", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.13.tgz", - "integrity": "sha512-23tS4oNL8nvkHcI4l9rbruQs2WS4yqQmBVQxWakqS9cmRpArLGgveR+hKNU5tPXm5EAi8oLO34/Zy7z70jUpCg==", + "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/compiler": "21.2.13", - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0 || ~0.16.0" - }, - "peerDependenciesMeta": { - "@angular/compiler": { - "optional": true - }, - "zone.js": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@angular/forms": { - "version": "21.2.13", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.13.tgz", - "integrity": "sha512-efAKdL8eVRlGvcJWrUFcYyRE/togWfopUTw2D5TIkDAndnmmRaWA70wD4n/E1FFV5UdxSBxoyEYE0qVlPiewtQ==", + "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.2.13", - "@angular/core": "21.2.13", - "@angular/platform-browser": "21.2.13", - "rxjs": "^6.5.3 || ^7.4.0" + "node": ">=18" } }, - "node_modules/@angular/platform-browser": { - "version": "21.2.13", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.13.tgz", - "integrity": "sha512-96rcwLHsklqAYRuS2SEBOUdQS5PLkuUIEEIjpYu4rxU2PVvOMapJEImM/QBxrbwjnCgRbj/CivkgfjiR0R0wSA==", + "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/animations": "21.2.13", - "@angular/common": "21.2.13", - "@angular/core": "21.2.13" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "21.2.13", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.13.tgz", - "integrity": "sha512-VltLjoKi7lOIGtwkBy9jauV+JLU9PAaLU7/iIVxZBgucvV85xj7CA+//KOBzneQ5V+XtnpgVrdE9bHMFIhiH5Q==", + "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.2.13", - "@angular/compiler": "21.2.13", - "@angular/core": "21.2.13", - "@angular/platform-browser": "21.2.13" + "node": ">=18" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "node": ">=18" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@angular/build/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "node_modules/@angular/build/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=18" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "node_modules/@angular/build/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@angular/build/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", "bin": { - "parser": "bin/babel-parser.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=6.0.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "node_modules/@angular/cli": { + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.24.tgz", + "integrity": "sha512-lx8baI3gssBv+xuwEf5wvAoBi7XSIB1RRuAmSnuAO2E4CAlJmU4eiVhKxd/haXvONH6peYxFGH2N6mSrkkHfIQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@angular-devkit/architect": "0.2102.24", + "@angular-devkit/core": "21.2.24", + "@angular-devkit/schematics": "21.2.24", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.30.0", + "@schematics/angular": "21.2.24", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.5.1", + "parse5-html-rewriting-stream": "8.0.0", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.3.6" + }, + "bin": { + "ng": "bin/ng.js" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, + "node_modules/@angular/common": { + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.24.tgz", + "integrity": "sha512-+olMzBBJHIGrFTUxKRksqtQ+KzBTifLOF+Er0ppRfjbTyYwdq4jfgZ9YjDQa00F/8Jcy2xzM1Nrlq5sIbqxAhg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.24", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "node_modules/@angular/compiler": { + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.24.tgz", + "integrity": "sha512-kvhstROpI9gZZ4QUcNkJaqA2LNis8XVTBz3X5SaKZAveUMB6r5V2akYqEBZcXNS4xUfST/UttrUsVdRvpcIItw==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "node_modules/@angular/compiler-cli": { + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.24.tgz", + "integrity": "sha512-oUk8IgWG5NXH3ZyqzRPxs9ylfe6K0p+mP89y023+/AF4A1Fse3V+tfjRxvq5gnlSyJm3yRl74W5SwsCi1kxxxA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "7.29.7", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, "engines": { - "node": ">=0.1.90" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.24", + "typescript": ">=5.9 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, + "node_modules/@angular/core": { + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.24.tgz", + "integrity": "sha512-wCSCNrcBolr2iws6eX8yMnePXvXMHzi3kFla7EoHiKGJBhKHz4g2+p5f4z/Q5mHVCglTsXEOldDhgFTv5pDDoQ==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.24", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } } }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, + "node_modules/@angular/forms": { + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.24.tgz", + "integrity": "sha512-1hOdu2dOjcVvYtcgdheEfin0nGj74YETHKhhG1iB1foqb70J6P/TfyXzTaiWr56cF0Dek1T3f9BjKE/KEFDUbw==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "tslib": "^2.4.0" + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.24", + "@angular/core": "21.2.24", + "@angular/platform-browser": "21.2.24", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, + "node_modules/@angular/platform-browser": { + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.24.tgz", + "integrity": "sha512-ffxWfxhMeedWXNASBtEg8AShG8ccWA/48Vp/rQVEhlj/CeydXTNdGmiu6oERQ5my4nOk+xJJ4/BnkQ3fSozrfQ==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.2.24", + "@angular/common": "21.2.24", + "@angular/core": "21.2.24" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "license": "MIT" + }, + "node_modules/@babel/core/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/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/types": "^7.27.3" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/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/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "node_modules/@blazediff/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", + "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -1320,16 +1583,16 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -1337,103 +1600,103 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ - "mips64el" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ - "s390x" + "arm" ], "dev": true, "license": "MIT", @@ -1445,12 +1708,12 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -1462,61 +1725,180 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ - "x64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ - "arm64" + "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -1531,9 +1913,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -1548,9 +1930,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -1565,9 +1947,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -1582,9 +1964,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -1599,9 +1981,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -1667,9 +2049,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1717,9 +2099,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", "dev": true, "license": "MIT", "dependencies": { @@ -1729,7 +2111,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.2", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -1765,9 +2147,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1806,9 +2188,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -1861,13 +2243,13 @@ "optional": true }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" @@ -2478,13 +2860,13 @@ ] }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "dev": true, "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -2746,6 +3128,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2763,6 +3148,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2780,6 +3168,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2797,6 +3188,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2814,6 +3208,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2831,6 +3228,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2848,6 +3248,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2944,48 +3347,10 @@ "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", - "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", "dev": true, "license": "ISC", "dependencies": { @@ -3000,9 +3365,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3053,9 +3418,9 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3536,6 +3901,13 @@ "license": "MIT", "optional": true }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.4", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", @@ -4115,23 +4487,23 @@ ] }, "node_modules/@saehrimnir/druidjs": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@saehrimnir/druidjs/-/druidjs-0.7.3.tgz", - "integrity": "sha512-ROrGB3VqU2ombyJz9XlvDvMCHstuJ25+lPzV1HXYaP0Pp4uOkcaHZUMJlA0YNB/SM0s/Toc9n4gJFCs7LPclDQ==", - "license": "BSD-3-Clause", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@saehrimnir/druidjs/-/druidjs-0.9.0.tgz", + "integrity": "sha512-Cv0xL8A8LbKqPbEyWu/bBnUd7Esjd/m92Dqyl2kpj6x30yCA6rRAtPl5Iw0Ks8u3cE9/acAeYxC/ezaxewI2nA==", + "license": "LGPL-3.0-or-later", "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/@schematics/angular": { - "version": "21.2.11", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.11.tgz", - "integrity": "sha512-EqH12Fr3vaWFpsilFDFXkxwMIidEDZr5cGl0w2hDRG7DjXE2oRB/VXix8xmpuHkzJ40Jgew6hIc+bfbwQhFK1A==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.24.tgz", + "integrity": "sha512-eGBr4Yhg/A8Lt8QGtWxmXAyKboX8QBwbDG1hSyApQfHAh9iNFbV7mgWSmk0qPWvRJM38s0t3EEkEg79QZ98dWQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.11", - "@angular-devkit/schematics": "21.2.11", + "@angular-devkit/core": "21.2.24", + "@angular-devkit/schematics": "21.2.24", "jsonc-parser": "3.3.1" }, "engines": { @@ -4154,9 +4526,9 @@ } }, "node_modules/@sigstore/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.0.tgz", - "integrity": "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4164,9 +4536,9 @@ } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", - "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.2.tgz", + "integrity": "sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4206,27 +4578,20 @@ } }, "node_modules/@sigstore/verify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", - "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, - "license": "MIT" - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4275,14 +4640,15 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/d3": { @@ -4569,6 +4935,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -4590,13 +4963,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jasmine": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz", - "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4604,16 +4970,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@types/stats.js": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", @@ -4622,19 +4978,18 @@ "license": "MIT" }, "node_modules/@types/three": { - "version": "0.176.0", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.176.0.tgz", - "integrity": "sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==", + "version": "0.186.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.186.0.tgz", + "integrity": "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ==", "dev": true, "license": "MIT", "dependencies": { - "@dimforge/rapier3d-compat": "^0.12.0", + "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", - "@types/webxr": "*", - "@webgpu/types": "*", - "fflate": "~0.8.2", - "meshoptimizer": "~0.18.1" + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.3", + "meshoptimizer": "~1.1.1" } }, "node_modules/@types/webxr": { @@ -4644,32 +4999,21 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", - "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz", + "integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/type-utils": "8.46.4", - "@typescript-eslint/utils": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/type-utils": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4679,21 +5023,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.4", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.70.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "node_modules/@typescript-eslint/parser": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.1.tgz", + "integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4703,33 +5049,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.1.tgz", + "integrity": "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@typescript-eslint/tsconfig-utils": "^8.70.1", + "@typescript-eslint/types": "^8.70.1", + "debug": "^4.4.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", - "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4738,15 +5072,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz", + "integrity": "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4755,24 +5093,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz", + "integrity": "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4781,20 +5107,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", - "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz", + "integrity": "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4" + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4804,20 +5131,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "node_modules/@typescript-eslint/types": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.1.tgz", + "integrity": "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.4", - "eslint-visitor-keys": "^4.2.1" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4826,64 +5149,22 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/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/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", - "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz", + "integrity": "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4" + "@typescript-eslint/project-service": "8.70.1", + "@typescript-eslint/tsconfig-utils": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4893,20 +5174,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.1.tgz", + "integrity": "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", - "debug": "^4.3.4" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4916,18 +5197,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz", + "integrity": "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" + "@typescript-eslint/types": "8.70.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4937,583 +5219,296 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "node_modules/@typescript-eslint/visitor-keys/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", + "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "node_modules/@vitest/browser": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.11.tgz", + "integrity": "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.0", + "ws": "^8.19.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.4", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "vitest": "4.1.11" } }, - "node_modules/@typescript-eslint/parser/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "node_modules/@vitest/browser-playwright": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.11.tgz", + "integrity": "sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/parser/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" + "@vitest/browser": "4.1.11", + "@vitest/mocker": "4.1.11", + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "url": "https://opencollective.com/vitest" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.11" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependenciesMeta": { + "playwright": { + "optional": false + } } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", - "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.4", - "@typescript-eslint/types": "^8.59.4", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.11", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", - "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", - "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", - "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, + "license": "MIT" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", - "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">= 0.6" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.4", - "eslint-visitor-keys": "^4.2.1" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=0.4.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/type-utils/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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 14" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", - "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", - "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.4", - "@typescript-eslint/tsconfig-utils": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", - "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/typescript-estree": "8.59.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", - "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.4", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@webgpu/types": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", - "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ajv-formats": { @@ -5561,24 +5556,25 @@ } }, "node_modules/angular-eslint": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.0.1.tgz", - "integrity": "sha512-YZT5SAYXv3zxTcCwjuAJb7D8tOnRyF+a7Hrn4niLAihH+DowBocOAvhWyjCDKEULNGDszcKZukaDMzp2S18I5A==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.4.0.tgz", + "integrity": "sha512-LH7bWmtJvsubzwPoztnl1pWgI5X0VrfGTUITGSYcwn2J+SXuN/avzrKrxJmhUiIrNvLtfV+18GG6xZS1IGZdKg==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/builder": "21.0.1", - "@angular-eslint/eslint-plugin": "21.0.1", - "@angular-eslint/eslint-plugin-template": "21.0.1", - "@angular-eslint/schematics": "21.0.1", - "@angular-eslint/template-parser": "21.0.1", + "@angular-eslint/builder": "21.4.0", + "@angular-eslint/eslint-plugin": "21.4.0", + "@angular-eslint/eslint-plugin-template": "21.4.0", + "@angular-eslint/schematics": "21.4.0", + "@angular-eslint/template-parser": "21.4.0", "@typescript-eslint/types": "^8.0.0", "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*", "typescript-eslint": "^8.0.0" } @@ -5628,33 +5624,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5672,6 +5641,35 @@ "node": ">= 0.4" } }, + "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-v8-to-istanbul": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", + "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", + "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", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5692,20 +5690,10 @@ "node": "18 || 20 || >=22" } }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, "node_modules/baseline-browser-mapping": { - "version": "2.10.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", - "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5736,36 +5724,37 @@ "node": ">=18.0.0" } }, - "node_modules/binary-extensions": { + "node_modules/body-parser": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dev": true, "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, "engines": { "node": ">=18" }, @@ -5782,35 +5771,22 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" + "node": "20 || >=22" } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.1.tgz", + "integrity": "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==", "dev": true, "funding": [ { @@ -5828,11 +5804,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.25", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.438", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -5899,9 +5875,9 @@ } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5950,9 +5926,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001812", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001812.tgz", + "integrity": "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==", "dev": true, "funding": [ { @@ -5970,6 +5946,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6183,113 +6169,28 @@ "dev": true, "license": "MIT" }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, "engines": { - "node": ">= 0.10.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/connect/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "engines": { + "node": ">= 0.6" } }, "node_modules/convert-source-map": { @@ -6382,13 +6283,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", - "dev": true, - "license": "MIT" - }, "node_modules/d3": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", @@ -6802,16 +6696,6 @@ "node": ">=12" } }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6856,17 +6740,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -6878,26 +6751,6 @@ "node": ">=8" } }, - "node_modules/di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -6980,9 +6833,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.360", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", - "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", + "version": "1.5.438", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.438.tgz", + "integrity": "sha512-AN9xMU1hJiT65LkCUPL1DZm5TCbOPb2Qsm5pYwFLEXp6/qj9SdT4yMiqs7Qqstwvkn1zF7l36SRGt+s6XcJ0FA==", "dev": true, "license": "ISC" }, @@ -7003,101 +6856,6 @@ "node": ">= 0.8" } }, - "node_modules/engine.io": { - "version": "6.6.8", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz", - "integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "@types/ws": "^8.5.12", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.20.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", - "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "punycode": "^1.4.1", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -7134,13 +6892,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -7161,6 +6912,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "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", @@ -7175,9 +6933,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7188,32 +6946,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { @@ -7247,9 +7005,10 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { @@ -7258,8 +7017,8 @@ "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7379,9 +7138,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -7487,1840 +7246,1045 @@ "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/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/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "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/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-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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-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/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/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/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/gsap": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", - "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", - "license": "Standard 'no charge' license: https://gsap.com/standard-license." - }, - "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-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.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.21", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz", - "integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "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/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-walk": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", - "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minimatch": "^10.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/immutable": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", - "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true, - "license": "MIT" - }, - "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", + "estraverse": "^5.1.0" + }, "engines": { - "node": ">=0.8.19" + "node": ">=0.10" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "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": "ISC", + "license": "BSD-2-Clause", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "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": "ISC" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "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": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" } }, - "node_modules/internmap": { + "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", + "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": ">=12" + "node": ">=0.10.0" } }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 0.6" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, "engines": { - "node": ">= 0.10" + "node": ">=18.0.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", "dev": true, "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.1" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=18" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "debug": "^4.4.3", + "ip-address": "^10.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">=16.0.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "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==", + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 18.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 8.0.0" + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" + "engines": { + "node": ">=16" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.8" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "BSD-3-Clause", + "license": "ISC", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">=10" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "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": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "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": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/jasmine-core": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz", - "integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT" + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/panva" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "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": { - "argparse": "^2.0.1" + "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" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", - "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "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": "MIT", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=10.13.0" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, "license": "BSD-2-Clause" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "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", - "bin": { - "json5": "lib/cli.js" - }, "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "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": "MIT" + "license": "ISC" }, - "node_modules/jsonfile": { + "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=8" } }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "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, - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/karma": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", - "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { - "@colors/colors": "1.5.0", - "body-parser": "^1.19.0", - "braces": "^3.0.2", - "chokidar": "^3.5.1", - "connect": "^3.7.0", - "di": "^0.0.1", - "dom-serialize": "^2.2.1", - "glob": "^7.1.7", - "graceful-fs": "^4.2.6", - "http-proxy": "^1.18.1", - "isbinaryfile": "^4.0.8", - "lodash": "^4.17.21", - "log4js": "^6.4.1", - "mime": "^2.5.2", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.5", - "qjobs": "^1.2.0", - "range-parser": "^1.2.1", - "rimraf": "^3.0.2", - "socket.io": "^4.7.2", - "source-map": "^0.6.1", - "tmp": "^0.2.1", - "ua-parser-js": "^0.7.30", - "yargs": "^16.1.1" - }, - "bin": { - "karma": "bin/karma" + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 10" + "node": ">= 0.4" } }, - "node_modules/karma-chrome-launcher": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", - "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "node_modules/hono": { + "version": "4.13.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.9.tgz", + "integrity": "sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg==", "dev": true, "license": "MIT", - "dependencies": { - "which": "^1.2.1" + "engines": { + "node": ">=16.9.0" } }, - "node_modules/karma-chrome-launcher/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "lru-cache": "^11.1.0" }, - "bin": { - "which": "bin/which" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/karma-coverage": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", - "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "MIT", - "dependencies": { - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.0.5", - "minimatch": "^3.0.4" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10.0.0" + "node": "20 || >=22" } }, - "node_modules/karma-coverage/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, - "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=8" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, - "license": "ISC", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": "*" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/karma-coverage/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/karma-jasmine": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", - "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "jasmine-core": "^4.1.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=12" - }, - "peerDependencies": { - "karma": "^6.0.0" + "node": ">= 14" } }, - "node_modules/karma-jasmine-html-reporter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", - "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", - "peerDependencies": { - "jasmine-core": "^4.0.0 || ^5.0.0", - "karma": "^6.0.0", - "karma-jasmine": "^5.0.0" + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" } }, - "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", - "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/karma/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { - "node": ">=8" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/karma/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 4" + } }, - "node_modules/karma/node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "minimatch": "^10.0.3" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/karma/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "license": "MIT" }, - "node_modules/karma/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">=6" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/karma/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/karma/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "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", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=0.8.19" } }, - "node_modules/karma/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/karma/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "dev": true, "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, "engines": { - "node": ">= 6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/karma/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, + "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==", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/karma/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 12" } }, - "node_modules/karma/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.10" } }, - "node_modules/karma/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/karma/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "get-east-asian-width": "^1.3.1" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/karma/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "is-extglob": "^2.1.1" }, "engines": { - "node": "*" + "node": ">=0.10.0" } }, - "node_modules/karma/node_modules/ms": { + "node_modules/is-interactive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, "license": "MIT" }, - "node_modules/karma/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/karma/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/karma/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "picomatch": "^2.2.1" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=8.10.0" + "node": ">=10" } }, - "node_modules/karma/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/karma/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/karma/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/panva" } }, - "node_modules/karma/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "argparse": "^2.0.1" }, - "engines": { - "node": ">= 0.6" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/karma/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=6" } }, - "node_modules/karma/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", "dev": true, "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, "engines": { - "node": ">=10" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/karma/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">=10" + "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9498,13 +8462,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9615,23 +8572,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9652,6 +8592,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -9669,9 +8621,9 @@ } }, "node_modules/make-fetch-happen": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", - "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", "dev": true, "license": "ISC", "dependencies": { @@ -9709,13 +8661,17 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/merge-descriptors": { @@ -9731,63 +8687,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/meshoptimizer": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", - "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "dev": true, "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -9829,13 +8735,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -9844,16 +8750,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -9987,19 +8883,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -10062,9 +8945,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -10088,13 +8971,34 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "dev": true, "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/node-addon-api": { @@ -10106,9 +9010,9 @@ "optional": true }, "node_modules/node-gyp": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, "license": "MIT", "dependencies": { @@ -10157,9 +9061,9 @@ } }, "node_modules/node-gyp/node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", "dev": true, "license": "MIT", "engines": { @@ -10183,11 +9087,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.44", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", - "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nopt": { "version": "9.0.0", @@ -10205,16 +9112,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-bundled": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", @@ -10353,6 +9250,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -10487,9 +9398,9 @@ } }, "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.8.tgz", + "integrity": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", "dev": true, "license": "MIT", "engines": { @@ -10500,12 +9411,13 @@ } }, "node_modules/pacote": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", - "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.1.tgz", + "integrity": "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", @@ -10519,7 +9431,6 @@ "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" @@ -10631,16 +9542,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10669,9 +9570,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -10689,6 +9590,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10710,9 +9618,9 @@ } }, "node_modules/piscina": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", - "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", + "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", "dev": true, "license": "MIT", "engines": { @@ -10732,10 +9640,67 @@ "node": ">=16.20.0" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -10753,7 +9718,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -10831,24 +9796,10 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10857,45 +9808,21 @@ }, "engines": { "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pyodide": { - "version": "0.28.3", - "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.28.3.tgz", - "integrity": "sha512-rtCsyTU55oNGpLzSVuAd55ZvruJDEX8o6keSdWKN9jPeBVSNlynaKFG7eRqkiIgU7i2M6HEgYtm0atCEQX3u4A==", - "license": "MPL-2.0", - "dependencies": { - "ws": "^8.5.0" }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.9" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -10904,27 +9831,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10972,16 +9878,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -10992,13 +9888,6 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -11026,27 +9915,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -11054,23 +9922,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -11174,32 +10025,8 @@ "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" + "engines": { + "node": ">= 18" } }, "node_modules/rw": { @@ -11217,24 +10044,6 @@ "tslib": "^2.1.0" } }, - "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/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11383,15 +10192,15 @@ } }, "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -11458,6 +10267,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -11472,23 +10288,38 @@ } }, "node_modules/sigstore": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", - "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", "dev": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.1.0", - "@sigstore/tuf": "^4.0.1", - "@sigstore/verify": "^3.1.0" + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -11530,101 +10361,10 @@ "npm": ">= 3.0.0" } }, - "node_modules/socket.io": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", - "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz", - "integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "~4.4.1", - "ws": "~8.20.1" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11711,9 +10451,9 @@ } }, "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==", + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", "dev": true, "license": "CC0-1.0" }, @@ -11730,6 +10470,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -11740,6 +10487,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", @@ -11753,21 +10507,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/streamroller": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", - "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -11838,9 +10577,9 @@ } }, "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -11865,9 +10604,16 @@ } }, "node_modules/three": { - "version": "0.176.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.176.0.tgz", - "integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==", + "version": "0.186.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.0.tgz", + "integrity": "sha512-cr/fIM2ddMSVbYVgkfD4jLJv7Fh/8ZTjvo+7gQeSVGUZHxpx9FDwoL5iC7hUz/LiRA8wMbqfnb90xKfm1/HHkQ==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, "license": "MIT" }, "node_modules/tinyexec": { @@ -11897,27 +10643,14 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tmp": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, "engines": { - "node": ">=8.0" + "node": ">=14.0.0" } }, "node_modules/toidentifier": { @@ -11930,366 +10663,144 @@ "node": ">=0.6" } }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tuf-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", - "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "4.1.0", - "debug": "^4.4.3", - "make-fetch-happen": "^15.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", - "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.4", - "@typescript-eslint/parser": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "node": ">=6" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.12" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", - "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4" + "prelude-ls": "^1.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">= 0.8.0" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "eslint-visitor-keys": "^4.2.1" + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/express" } }, - "node_modules/typescript-eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/typescript-eslint/node_modules/brace-expansion": { + "node_modules/type-is/node_modules/content-type": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/typescript-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" + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/typescript-eslint/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=14.17" } }, - "node_modules/ua-parser-js": { - "version": "0.7.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", - "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "node_modules/typescript-eslint": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.1.tgz", + "integrity": "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.1", + "@typescript-eslint/parser": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1" }, "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -12301,9 +10812,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -12351,16 +10862,6 @@ "node": ">=6" } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/validate-npm-package-name": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", @@ -12381,14 +10882,169 @@ "node": ">= 0.8" } }, - "node_modules/void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=0.10.0" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, "node_modules/watchpack": { @@ -12429,6 +11085,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -12517,9 +11190,10 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12630,9 +11304,9 @@ } }, "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", "dev": true, "license": "MIT", "engines": { @@ -12676,9 +11350,9 @@ } }, "node_modules/zone.js": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", - "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.3.tgz", + "integrity": "sha512-ihXL9+vhYyEhXz4TDNpHeAOZN9FVrbog0Il64OIEI28UP/n5AaI6gsccRuOHBfx+206agzyoK527bYIO0Foy6A==", "license": "MIT" } } diff --git a/package.json b/package.json index 174873d..f85a505 100644 --- a/package.json +++ b/package.json @@ -7,55 +7,48 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "deploy": "ng build --base-ref /glyphspace/", + "test:ci": "ng test --watch=false", "lint": "ng lint", "format": "prettier --write \"src/**/*.{ts,html,scss,json}\"", "format:check": "prettier --check \"src/**/*.{ts,html,scss,json}\"", "prepare": "husky" }, "private": true, + "engines": { + "node": "^24.15.0 || >=26.0.0" + }, "dependencies": { - "@angular/common": "^21.1.4", - "@angular/compiler": "^21.1.4", - "@angular/core": "^21.1.4", - "@angular/forms": "^21.1.4", - "@angular/platform-browser": "^21.1.4", - "@angular/platform-browser-dynamic": "^21.1.4", - "@saehrimnir/druidjs": "^0.7.3", + "@angular/common": "^21.2.24", + "@angular/compiler": "^21.2.24", + "@angular/core": "^21.2.24", + "@angular/forms": "^21.2.24", + "@angular/platform-browser": "^21.2.24", + "@saehrimnir/druidjs": "^0.9.0", "d3": "^7.9.0", - "d3-force": "^3.0.0", - "gsap": "^3.15.0", "material-icons": "^1.13.14", - "pyodide": "^0.28.0", "rxjs": "~7.8.0", - "three": "^0.176.0", + "three": "^0.186.0", "tslib": "^2.3.0", - "zone.js": "~0.15.0" + "zone.js": "~0.16.3" }, "devDependencies": { - "@angular/build": "^21.1.4", - "@angular/cli": "^21.1.4", - "@angular/compiler-cli": "^21.1.4", + "@angular/build": "^21.2.24", + "@angular/cli": "^21.2.24", + "@angular/compiler-cli": "^21.2.24", "@types/d3": "^7.4.3", - "@types/d3-force": "^3.0.10", - "@types/d3-quadtree": "^3.0.6", - "@types/jasmine": "~5.1.0", - "@types/node": "^18.18.0", - "@types/three": "^0.176.0", - "angular-eslint": "21.0.1", - "eslint": "^9.39.1", + "@types/three": "^0.186.0", + "@vitest/browser-playwright": "^4.1.11", + "@vitest/coverage-v8": "^4.1.11", + "angular-eslint": "~21.4.0", + "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", - "jasmine-core": "~5.6.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.1.0", "lint-staged": "^16.2.7", + "playwright": "^1.62.1", "prettier": "^3.8.1", "typescript": "~5.9.3", - "typescript-eslint": "8.46.4" + "typescript-eslint": "^8.70.1", + "vitest": "^4.1.11" }, "lint-staged": { "src/**/*.{ts,html}": [ diff --git a/src/app/canvas/glyph-canvas.component.ts b/src/app/canvas/glyph-canvas.component.ts index 0b0764a..d7f0618 100644 --- a/src/app/canvas/glyph-canvas.component.ts +++ b/src/app/canvas/glyph-canvas.component.ts @@ -20,7 +20,7 @@ import { DataLoaderService } from '../services/data-loader.service'; import { FilterService } from '../services/filter.service'; import { GlyphObject } from '../glyph/glyph-object'; import { ZoomLevel } from '../shared/enum/zoom-level'; -import { forceCollide, forceSimulation, Simulation } from 'd3-force'; +import { forceCollide, forceSimulation, Simulation } from 'd3'; import { clusterGlyphs, getGlyphFromObject } from '../shared/helpers/glyph-helper'; import { InteractionCommand } from '../shared/enum/interaction-command'; import { GlyphCacheObject } from '../glyph/glyph-cache-object'; diff --git a/src/app/canvas/magiclens/magiclens.component.ts b/src/app/canvas/magiclens/magiclens.component.ts index 2938d6e..5477dc8 100644 --- a/src/app/canvas/magiclens/magiclens.component.ts +++ b/src/app/canvas/magiclens/magiclens.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, Input, ViewChild, AfterViewInit, OnDestroy } from '@angular/core'; -import { forceSimulation, forceCollide } from 'd3-force'; +import { forceSimulation, forceCollide } from 'd3'; import * as THREE from 'three'; import { GlyphObject } from '../../glyph/glyph-object'; import { ZoomLevel } from '../../shared/enum/zoom-level'; diff --git a/src/app/canvas/services/canvas-renderer.service.ts b/src/app/canvas/services/canvas-renderer.service.ts index 608052d..bc657c8 100644 --- a/src/app/canvas/services/canvas-renderer.service.ts +++ b/src/app/canvas/services/canvas-renderer.service.ts @@ -12,7 +12,7 @@ import { scalePosition, screenToWorld, } from '../../shared/helpers/three-helper'; -import { LENS_ENLARGEMENT_FACTOR } from '../../shared/constants/canvas-constants'; +import { LENS_ENLARGEMENT_FACTOR, MIN_RENDER_INTERVAL_MS } from '../../shared/constants/canvas-constants'; import { GlyphRenderConfig } from '../../glyph/glyph-render-config'; // Reusable scratch vector to avoid per-frame allocations @@ -45,7 +45,6 @@ export class CanvasRendererService { // Render throttling to prevent cascading re-renders private lastRenderTime = 0; private pendingRender: ReturnType | null = null; - private readonly MIN_RENDER_INTERVAL_MS = 50; setRenderRequestCallback(fn: () => void): void { this.onRenderRequested = fn; @@ -99,7 +98,7 @@ export class CanvasRendererService { const elapsed = now - this.lastRenderTime; // Throttle: if called too rapidly, schedule a single trailing render - if (elapsed < this.MIN_RENDER_INTERVAL_MS && !force) { + if (elapsed < MIN_RENDER_INTERVAL_MS && !force) { if (!this.pendingRender) { this.pendingRender = setTimeout(() => { this.pendingRender = null; @@ -112,7 +111,7 @@ export class CanvasRendererService { renderConfig, force ); - }, this.MIN_RENDER_INTERVAL_MS - elapsed); + }, MIN_RENDER_INTERVAL_MS - elapsed); } return; } diff --git a/src/app/canvas/settingscontrols/settingscontrols.component.ts b/src/app/canvas/settingscontrols/settingscontrols.component.ts index 0e31234..abf0093 100644 --- a/src/app/canvas/settingscontrols/settingscontrols.component.ts +++ b/src/app/canvas/settingscontrols/settingscontrols.component.ts @@ -5,7 +5,7 @@ import { FormsModule } from '@angular/forms'; import { FilterService } from '../../services/filter.service'; import { ProjectionService } from '../../services/projection.service'; import { Subscription } from 'rxjs'; -import { COLOR_SCALES, getContinuousGradient, getCategoricalColors } from '../../shared/interfaces/color-scale'; +import { getColorScaleById, getContinuousGradient, getCategoricalColors } from '../../shared/interfaces/color-scale'; import { getGlyphTypeName } from '../../shared/enum/glyph-type'; export type SettingMode = 'position' | null; @@ -199,7 +199,7 @@ export class SettingsControlPanelComponent implements OnDestroy, OnInit { return; } - const colorScale = COLOR_SCALES.find(cs => cs.id === colorRange) || COLOR_SCALES[0]; + const colorScale = getColorScaleById(colorRange); let gradient: string; if (colorScale.type === 'categorical') { diff --git a/src/app/canvas/tooltip/tooltip.component.html b/src/app/canvas/tooltip/tooltip.component.html index 9a73217..2a2be0e 100644 --- a/src/app/canvas/tooltip/tooltip.component.html +++ b/src/app/canvas/tooltip/tooltip.component.html @@ -4,5 +4,20 @@ [style.left.px]="tooltipX" [style.top.px]="tooltipY" [style.display]="tooltipVisible || tooltipFixed ? 'block' : 'none'" - [innerHTML]="tooltipText" -> +> + @if (tooltipInfo; as info) { +
    + Item: {{ info.id }} + + @for (row of info.rows; track row.key) { + + + + + } +
    {{ row.label }}{{ row.value ?? 'missing' }}
    +
    + } @else { + Unnamed object + } + diff --git a/src/app/canvas/tooltip/tooltip.component.scss b/src/app/canvas/tooltip/tooltip.component.scss index 0f864b4..ee5a4c9 100644 --- a/src/app/canvas/tooltip/tooltip.component.scss +++ b/src/app/canvas/tooltip/tooltip.component.scss @@ -22,3 +22,36 @@ background: rgba(0, 0, 0, 1); } } + +.tooltip-body { + max-width: 300px; + font-family: sans-serif; + font-size: 13px; + + table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + margin-top: 0.4em; + } + + th, + td { + padding: 0; + vertical-align: top; + text-align: left; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } + + th { + width: 40%; + padding-right: 0.75em; + } + + td.missing { + font-style: italic; + opacity: 0.7; + } +} diff --git a/src/app/canvas/tooltip/tooltip.component.spec.ts b/src/app/canvas/tooltip/tooltip.component.spec.ts new file mode 100644 index 0000000..fb4e866 --- /dev/null +++ b/src/app/canvas/tooltip/tooltip.component.spec.ts @@ -0,0 +1,58 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { TooltipComponent, buildTooltipRows } from './tooltip.component'; +import { ConfigService } from '../../services/config.service'; + +describe('buildTooltipRows', () => { + it('labels each value and marks missing ones', () => { + const rows = buildTooltipRows({ '1': '7.4', '2': '', '3': 'nan', '4': '0' }, { '1': 'acidity', '2': 'sugar' }); + + expect(rows).toEqual([ + { key: '1', label: 'acidity', value: '7.4' }, + { key: '2', label: 'sugar', value: null }, + { key: '3', label: '3', value: null }, + { key: '4', label: '4', value: '0' }, + ]); + }); +}); + +describe('TooltipComponent', () => { + let fixture: ComponentFixture; + let host: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TooltipComponent], + providers: [ + { provide: ConfigService, useValue: { modalOpen: false, featureLabels: {}, modalOpenSubject$: of(false) } }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(TooltipComponent); + host = fixture.nativeElement as HTMLElement; + }); + + // Regression: values, labels and ids come from uploaded CSV files and were + // interpolated into HTML trusted via bypassSecurityTrustHtml (stored XSS). + it('renders CSV content as text, never as markup', () => { + const payload = ''; + fixture.componentInstance.tooltipInfo = { + id: payload, + rows: [{ key: '1', label: payload, value: payload }], + }; + fixture.componentInstance.tooltipVisible = true; + fixture.detectChanges(); + + expect(host.querySelector('img')).toBeNull(); + expect(host.textContent).toContain(payload); + }); + + it('shows "missing" instead of a value for missing entries', () => { + fixture.componentInstance.tooltipInfo = { id: 'a', rows: [{ key: '1', label: 'sugar', value: null }] }; + fixture.componentInstance.tooltipVisible = true; + fixture.detectChanges(); + + const cell = host.querySelector('td'); + expect(cell?.textContent?.trim()).toBe('missing'); + expect(cell?.classList).toContain('missing'); + }); +}); diff --git a/src/app/canvas/tooltip/tooltip.component.ts b/src/app/canvas/tooltip/tooltip.component.ts index 234e153..fb3771d 100644 --- a/src/app/canvas/tooltip/tooltip.component.ts +++ b/src/app/canvas/tooltip/tooltip.component.ts @@ -2,9 +2,34 @@ import * as THREE from 'three'; import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { getGlyphFromObject } from '../../shared/helpers/glyph-helper'; import { ConfigService } from '../../services/config.service'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { Subscription } from 'rxjs'; +/** One tooltip line; `value` is null when the original data had no value. */ +export interface TooltipRow { + key: string; + label: string; + value: string | null; +} + +export interface TooltipInfo { + id: string; + rows: TooltipRow[]; +} + +// '' is what the processors write for a missing value; 'nan' is what older CLI output contains. +const MISSING_VALUES = new Set(['', 'nan']); + +export function buildTooltipRows( + values: Record, + labels: Record | undefined +): TooltipRow[] { + return Object.entries(values).map(([key, value]) => ({ + key, + label: labels?.[key] ?? key, + value: MISSING_VALUES.has(value.trim().toLowerCase()) ? null : value, + })); +} + @Component({ selector: 'app-tooltip', imports: [], @@ -19,14 +44,12 @@ export class TooltipComponent implements OnInit, OnDestroy { private subscription = new Subscription(); tooltipVisible = false; tooltipFixed = false; - tooltipText: SafeHtml = ''; + // Rendered by the template, so CSV content (ids, labels, values) is always escaped. + tooltipInfo: TooltipInfo | null = null; tooltipX = 0; tooltipY = 0; - constructor( - private config: ConfigService, - private sanitizer: DomSanitizer - ) {} + constructor(private config: ConfigService) {} ngOnInit(): void { // Subscribe to modal state changes to hide fixed tooltips when modal opens @@ -43,12 +66,12 @@ export class TooltipComponent implements OnInit, OnDestroy { this.subscription.unsubscribe(); } - showTooltip(x: number, y: number, text: string): void { + showTooltip(x: number, y: number, info: TooltipInfo | null): void { // Don't show tooltip if a modal (like preprocessing wizard) is open if (this.config.modalOpen) { return; } - this.tooltipText = this.sanitizer.bypassSecurityTrustHtml(text); + this.tooltipInfo = info; this.tooltipX = x + 10; // slight offset from cursor this.tooltipY = y + 10; this.tooltipVisible = true; @@ -137,41 +160,11 @@ export class TooltipComponent implements OnInit, OnDestroy { return this.tooltipFixed; } - getObjectInfo(object: THREE.Object3D): string { + getObjectInfo(object: THREE.Object3D): TooltipInfo | null { const glyph = getGlyphFromObject(object); - const labels = this.config.featureLabels; - - if (glyph !== null) { - const rows = Object.entries(glyph.values || {}) - .map(([key, value]) => { - const label = labels?.[key] ?? key; - return ` - - - ${label} - - - ${value} - - `; - }) - .join(''); - - return ` -
    - Item: ${glyph.id} - - ${rows} -
    -
    - `; + if (glyph === null) { + return null; } - - return 'Unnamed object'; + return { id: glyph.id, rows: buildTooltipRows(glyph.values ?? {}, this.config.featureLabels) }; } } diff --git a/src/app/glyph/glyph-cache-object.ts b/src/app/glyph/glyph-cache-object.ts index a8ca252..eecddff 100644 --- a/src/app/glyph/glyph-cache-object.ts +++ b/src/app/glyph/glyph-cache-object.ts @@ -1,4 +1,4 @@ -import { SimulationNodeDatum } from 'd3-force'; +import { SimulationNodeDatum } from 'd3'; import { Coordinates } from '../shared/interfaces/coordinates'; import { Object3D } from 'three'; diff --git a/src/app/menubar/menubar.component.html b/src/app/menubar/menubar.component.html index 49b5094..a96f86c 100644 --- a/src/app/menubar/menubar.component.html +++ b/src/app/menubar/menubar.component.html @@ -8,8 +8,15 @@ - @@ -23,14 +30,20 @@
    Data
    - @for (entry of datasetEntries; track entry.name) { } @if (isUserDataset(selectedDataset)) { - }
    @@ -38,18 +51,18 @@
    - - -
    @@ -57,12 +70,12 @@
    Canvas - -
    @@ -72,6 +85,12 @@ @if (showWizard) {
    - + + @defer (on immediate) { + + } @loading (after 150ms) { +

    Loading wizard…

    + }
    } diff --git a/src/app/menubar/menubar.component.scss b/src/app/menubar/menubar.component.scss index bb63189..75d2f68 100644 --- a/src/app/menubar/menubar.component.scss +++ b/src/app/menubar/menubar.component.scss @@ -1,4 +1,5 @@ @use '../../styles/variables' as v; +@use '../../styles/mixins' as m; .menu-bar { display: flex; @@ -123,6 +124,10 @@ &:focus { outline: none; } + + &:focus-visible { + @include m.focus-ring; + } } /* Optional: add a custom arrow */ @@ -138,6 +143,24 @@ } } +// Shared look of the toolbar buttons (icon panel and canvas controls). +// Include it after the caller's own declarations so they stay in one rule. +@mixin toolbar-button { + align-items: center; + border: none; + cursor: pointer; + font-size: 14px; + color: v.$gray-800; + + &:hover { + background: v.$gray-200; + } + + .material-icons { + font-size: 18px; + } +} + .icon-panel { display: flex; gap: 6px; @@ -147,25 +170,11 @@ button { display: flex; - align-items: center; gap: 6px; - - border: none; background: transparent; padding: 6px 10px; border-radius: 8px; - cursor: pointer; - - font-size: 14px; - color: v.$gray-800; - - &:hover { - background: v.$gray-200; - } - - .material-icons { - font-size: 18px; - } + @include toolbar-button; } } @@ -180,25 +189,12 @@ .add-btn { display: inline-flex; - align-items: center; gap: 4px; white-space: nowrap; - - border: none; padding: 6px 12px; border-radius: 10px; background: v.$gray-100; - cursor: pointer; - font-size: 14px; - color: v.$gray-800; - - &:hover { - background: v.$gray-200; - } - - .material-icons { - font-size: 18px; - } + @include toolbar-button; } } @@ -242,11 +238,10 @@ top: 64px; left: 0; right: 0; - z-index: 920; + z-index: v.$z-menu; background: white; max-height: 0; overflow: hidden; - transition: max-height 400ms ease; transition: height 400ms ease; } @@ -297,7 +292,7 @@ right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); - z-index: 9999; + z-index: v.$z-overlay; display: flex; align-items: center; justify-content: center; @@ -316,6 +311,38 @@ animation: slideUp 0.3s ease; overflow: hidden; } + + // Shown by @defer while the lazy wizard chunk loads (after 150ms). + .wizard-loading { + margin: 0; + padding: v.$space-2 v.$space-4; + border-radius: 8px; + background: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + color: v.$gray-700; + font-size: v.$font-size-md; + } + + @media (prefers-reduced-motion: reduce) { + &, + app-preprocessing-wizard { + animation: none; + } + } + + // Small viewports / 200% zoom (1280x800 -> 640x400 CSS px): the wizard takes + // the whole viewport instead of 90% minus the 32px backdrop padding. + @media (max-width: 767.98px), (max-height: 600px) { + padding: 0; + + app-preprocessing-wizard { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + border-radius: 0; + } + } } @keyframes fadeIn { diff --git a/src/app/menubar/menubar.component.spec.ts b/src/app/menubar/menubar.component.spec.ts new file mode 100644 index 0000000..e951ad5 --- /dev/null +++ b/src/app/menubar/menubar.component.spec.ts @@ -0,0 +1,77 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; +import { MenuBarComponent } from './menubar.component'; +import { DataLoaderService } from '../services/data-loader.service'; +import { FilterService } from '../services/filter.service'; +import { DataExportService } from '../services/data-export.service'; +import { ConfigService } from '../services/config.service'; + +/** The menubar hosts the wizard dialog: opening, closing and returning focus (WCAG 2.4.3). */ +describe('MenuBarComponent – wizard dialog host', () => { + let fixture: ComponentFixture; + let el: HTMLElement; + const config = { modalOpen: false, loadedDataSubject$: new BehaviorSubject('') }; + + function uploadButton(): HTMLButtonElement { + const button = Array.from(el.querySelectorAll('.icon-panel button')).find(b => + b.textContent?.includes('Upload') + ); + if (!button) throw new Error('Upload button not rendered'); + return button; + } + + beforeEach(async () => { + config.modalOpen = false; + await TestBed.configureTestingModule({ + imports: [MenuBarComponent], + providers: [ + { provide: DataLoaderService, useValue: { dataSetCollectionSubject$: new BehaviorSubject([]) } }, + { provide: FilterService, useValue: {} }, + { provide: DataExportService, useValue: {} }, + { provide: ConfigService, useValue: config }, + ], + }) + .overrideComponent(MenuBarComponent, { set: { imports: [FormsModule], schemas: [NO_ERRORS_SCHEMA] } }) + .compileComponents(); + fixture = TestBed.createComponent(MenuBarComponent); + el = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('announces that the Upload button opens a dialog', () => { + expect(uploadButton().getAttribute('aria-haspopup')).toBe('dialog'); + }); + + it('opens the wizard overlay from the Upload button', () => { + uploadButton().click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.showWizard).toBe(true); + expect(config.modalOpen).toBe(true); + expect(el.querySelector('.wizard-overlay')).not.toBeNull(); + }); + + it('returns focus to the Upload button when the wizard closes', () => { + // On narrow layouts Upload sits in the collapsible menu, which the user opened to reach it. + fixture.componentInstance.menuOpen = true; + fixture.detectChanges(); + uploadButton().click(); + fixture.detectChanges(); + (document.activeElement as HTMLElement | null)?.blur(); + + fixture.componentInstance.closePreprocessingWizard(); + fixture.detectChanges(); + + expect(fixture.componentInstance.showWizard).toBe(false); + expect(config.modalOpen).toBe(false); + expect(document.activeElement).toBe(uploadButton()); + }); + + it('hides the decorative icons from assistive technology', () => { + const icons = Array.from(el.querySelectorAll('.material-icons')); + expect(icons.length).toBeGreaterThan(0); + icons.forEach(icon => expect(icon.getAttribute('aria-hidden'), icon.textContent ?? '').toBe('true')); + }); +}); diff --git a/src/app/menubar/menubar.component.ts b/src/app/menubar/menubar.component.ts index 9360b33..bd6a86d 100644 --- a/src/app/menubar/menubar.component.ts +++ b/src/app/menubar/menubar.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Subscription } from 'rxjs'; import { PreprocessingWizardComponent } from '../preprocessing-wizard/preprocessing-wizard.component'; @@ -18,6 +18,9 @@ export class MenuBarComponent implements OnInit, OnDestroy { @Output() addCanvas = new EventEmitter(); @Input() totalCells = 0; + @ViewChild('uploadButton') uploadButton?: ElementRef; + @ViewChild('menuToggle') menuToggle?: ElementRef; + menuOpen = false; showWizard = false; hasData = false; @@ -69,6 +72,18 @@ export class MenuBarComponent implements OnInit, OnDestroy { closePreprocessingWizard() { this.showWizard = false; this.configService.modalOpen = false; + this.restoreFocusAfterWizard(); + } + + /** + * WCAG 2.4.3: closing the wizard dialog returns focus to the Upload button that + * opened it — or to the menu toggle when the (mobile) menu is collapsed. + */ + private restoreFocusAfterWizard(): void { + const toggle = this.menuToggle?.nativeElement; + const menuCollapsed = !this.menuOpen && !!toggle && toggle.getClientRects().length > 0; + const target = menuCollapsed ? toggle : this.uploadButton?.nativeElement; + target?.focus(); } download() { diff --git a/src/app/preprocessing-wizard/models/column-config.ts b/src/app/preprocessing-wizard/models/column-config.ts index bc3cb33..15dcc03 100644 --- a/src/app/preprocessing-wizard/models/column-config.ts +++ b/src/app/preprocessing-wizard/models/column-config.ts @@ -67,8 +67,7 @@ export interface ProjectionConfig { umapNeighbors: number; umapMinDist: number; - // TriMap parameters - trimapWeightAdj: number; // Weight adjustment factor (default: 500) + // TriMap has no parameters: it runs with DruidJS's defaults. } export interface CleaningResult { diff --git a/src/app/preprocessing-wizard/models/preprocessing-state.ts b/src/app/preprocessing-wizard/models/preprocessing-state.ts index b08433f..0387c7c 100644 --- a/src/app/preprocessing-wizard/models/preprocessing-state.ts +++ b/src/app/preprocessing-wizard/models/preprocessing-state.ts @@ -3,7 +3,8 @@ import { ColumnConfig, CleaningConfig, ProjectionConfig, CleaningResult } from ' import { DatasetCollection } from '../../shared/interfaces/dataset-collection'; export interface PreprocessingState { - // Current step (0-5) + // Index into WIZARD_STEPS (0 = upload … 5 = review). Resolve it through the + // step-id helpers in shared/constants/step-info.ts instead of comparing numbers. currentStep: number; // Data @@ -45,7 +46,7 @@ export interface ProcessingProgress { /** * A4 – Undo/Redo. Status of the history stack, published so the wizard shell can * enable/disable the toolbar buttons and show the concrete next action in the - * tooltip (e.g. "Rückgängig: Smart Defaults angewendet"). + * tooltip (e.g. "Undo: Recommended features applied (Ctrl+Z)"). */ export interface HistoryStatus { canUndo: boolean; @@ -88,7 +89,28 @@ export const DEFAULT_PROJECTION_CONFIG: ProjectionConfig = { // UMAP parameters umapNeighbors: 15, umapMinDist: 0.1, - - // TriMap parameters - trimapWeightAdj: 500, }; + +/** State of a wizard with nothing loaded yet. */ +export function createInitialState(): PreprocessingState { + return { + currentStep: 0, + rawFileName: null, + dataProfile: null, + columnConfigs: new Map(), + cleaningConfig: { ...DEFAULT_CLEANING_CONFIG }, + projectionConfig: { ...DEFAULT_PROJECTION_CONFIG }, + cleaningResult: null, + processedDataset: null, + datasetName: '', + timestamp: '', + glyphFeatures: [], + tooltipFeatures: [], + colorScaleMode: 'continuous', + colorScaleId: 0, + isProcessing: false, + processingProgress: 0, + processingStep: '', + error: null, + }; +} diff --git a/src/app/preprocessing-wizard/preprocessing-wizard.component.html b/src/app/preprocessing-wizard/preprocessing-wizard.component.html index b727d6d..e10cdbc 100644 --- a/src/app/preprocessing-wizard/preprocessing-wizard.component.html +++ b/src/app/preprocessing-wizard/preprocessing-wizard.component.html @@ -1,12 +1,57 @@ -
    + + +
    + + + +
    + +
    +
    + diff --git a/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.scss b/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.scss new file mode 100644 index 0000000..3491dc3 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.scss @@ -0,0 +1,490 @@ +@use '../../shared/wizard-shared' as w; +@use '../../../../styles/variables' as v; +@use '../../../../styles/mixins' as m; + +// A6: invisible scroll anchor used by the review step's deep-links +.scroll-anchor { + @include w.scroll-anchor; +} + +.step5-glyph-color-settings { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + + .step-content { + @include w.step-content; + } + + // Reminds that this step only seeds the dashboard's initial view. + .initial-view-note { + @include w.info-box; + margin-top: 0; + } + + // ============================================================================ + // Config Panel (shared base) + // ============================================================================ + + .config-panel { + @include w.config-panel; + } + + // ============================================================================ + // Color Feature Panel (compact dropdown) + // ============================================================================ + + .menu-label { + @include m.menu-label($size: v.$font-size-xs); + } + + // Label + help button side by side (the button must not sit inside the label). + .label-row { + display: flex; + align-items: center; + gap: v.$space-1; + } + + .color-feature-panel { + .panel-header-row { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: v.$space-4; + } + + .form-group { + @include m.form-group; + + // Native select must read as a dropdown (chevron affordance), not a + // plain text input (C-S4-02). + select { + @include w.select; + width: 200px; + max-width: 100%; + display: block; + } + + app-color-scale-selector { + width: 200px; + max-width: 100%; + display: block; + } + } + } + + // ============================================================================ + // Two-Column: Glyph Features + Preview + // ============================================================================ + + .glyph-section-two-col { + display: grid; + grid-template-columns: 3fr 2fr; + gap: v.$space-4; + margin-bottom: v.$space-4; + + @include w.below-stack { + grid-template-columns: minmax(0, 1fr); + } + } + + // ── Left: Glyph Features Panel ───────────────────────────────────────────── + + .glyph-features-panel { + display: flex; + flex-direction: column; + min-width: 0; + max-height: 480px; + overflow: hidden; + + // Stacked: grow with the content instead of clipping it; the two lists + // below keep their own bounded, scrolling heights. + @include w.below-stack { + max-height: none; + overflow: visible; + } + + .glyph-panel-header { + @include w.list-header-row; + flex-wrap: wrap; + justify-content: space-between; + gap: v.$space-2; + margin-bottom: v.$space-2; + + .heading-with-help { + display: flex; + align-items: center; + gap: v.$space-1; + } + + h3 { + margin-bottom: 0; + } + } + + .btn-suggestion-compact { + // Match the wizard's primary "Continue" nav button (accent, white text). + @include w.btn-primary; + font-size: v.$font-size-xs; + padding: v.$space-1 10px; + white-space: nowrap; + flex-shrink: 0; + + .material-icons { + font-size: v.$icon-size-xs; + } + } + + .feature-progress { + @include w.feature-progress; + margin-bottom: v.$space-2; + + .progress-fill.complete { + background: v.$severity-success-icon; + } + } + + .feature-list-unified { + display: flex; + flex-direction: column; + gap: 3px; + min-height: 50px; + max-height: 200px; + overflow-y: auto; + padding: 6px; + border: 2px dashed v.$gray-300; + border-radius: 8px; + background: v.$wizard-header-bg; + } + + // Search + type filter above the available list (same anatomy as steps 2/3) + .feature-filter-bar { + display: flex; + align-items: center; + gap: v.$space-2; + margin-bottom: 6px; + + .search-box { + @include w.search-box($padding: v.$space-1 v.$space-2, $icon-size: v.$icon-size-sm); + flex: 1; + min-width: 0; + } + + .filter-select { + @include w.select; + font-size: v.$font-size-sm; + padding: v.$space-1 v.$space-6 v.$space-1 v.$space-2; + flex-shrink: 0; + max-width: 130px; + } + } + + .available-empty { + @include w.list-empty-state; + } + + .empty-hint { + display: flex; + align-items: center; + gap: 6px; + padding: 10px v.$space-2; + color: v.$text-muted; + font-size: v.$font-size-sm; + + .material-icons { + font-size: v.$icon-size-sm; + } + } + + .feature-separator { + height: 1px; + background: v.$gray-300; + margin: v.$space-2 0; + } + + .available-label { + @include w.list-header-row; + justify-content: space-between; + font-size: v.$font-size-xs; + font-weight: 600; + color: v.$text-muted; + letter-spacing: 0.3px; + margin-bottom: v.$space-1; + padding: 0 v.$space-2; + + .variance-label { + font-size: v.$font-size-xs; + font-weight: 500; + color: v.$text-muted; + } + } + + .available-features-list { + display: flex; + flex-direction: column; + gap: 1px; + overflow-y: auto; + flex: 1; + min-height: 0; + max-height: 240px; + } + + .feature-row { + display: flex; + align-items: center; + gap: 6px; + padding: 5px v.$space-2; + border-radius: 6px; + font-size: v.$font-size-sm; + transition: background-color 0.15s ease; + + &.selected { + background: white; + border: 1px solid v.$gray-300; + cursor: grab; + + &:active { + cursor: grabbing; + } + + &:hover { + border-color: v.$color-accent; + } + + &.drop-target { + border-color: v.$color-accent; + box-shadow: inset 0 2px 0 v.$color-accent; + } + + .drag-handle { + font-size: v.$icon-size-sm; + color: v.$gray-500; + flex-shrink: 0; + cursor: grab; + } + + // White digit on the accent (5.60:1; was 2.30:1 on cyan). + .feature-order { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + background: v.$color-accent; + color: white; + font-size: v.$font-size-xs; + font-weight: 700; + border-radius: 50%; + flex-shrink: 0; + } + } + + &.available { + cursor: grab; + color: v.$gray-700; + + &:active, + &.dragging { + cursor: grabbing; + } + + &:hover { + background: v.$gray-100; + } + + .add-icon { + font-size: v.$icon-size-sm; + color: v.$gray-500; + flex-shrink: 0; + } + } + + .feature-name { + flex: 1; + font-weight: 500; + color: v.$gray-800; + @include m.text-truncate; + } + + // Data visualisation (variance share): decorative bright fill. + .variance-bar { + width: 40px; + height: 4px; + background: v.$gray-200; + border-radius: 2px; + flex-shrink: 0; + overflow: hidden; + + .variance-fill { + display: block; + height: 100%; + background: v.$active-color; + border-radius: 2px; + min-width: 2px; + } + } + + // Move up / move down / remove + .btn-row-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + color: v.$text-muted; + flex-shrink: 0; + + .material-icons { + font-size: v.$icon-size-xs; + } + + &:focus-visible { + @include m.focus-ring(1px); + } + + &:disabled { + opacity: 0.35; + cursor: default; + } + } + + .btn-move:hover:not(:disabled) { + background: rgba(v.$active-color, 0.12); + color: v.$color-accent; + } + + .btn-remove:hover { + background: v.$severity-error-text; + color: white; + } + } + + // Available features are real buttons (Enter and Space add them). + button.feature-row.available { + width: 100%; + border: none; + background: transparent; + font: inherit; + font-size: v.$font-size-sm; + text-align: left; + + &:hover { + background: v.$gray-100; + } + + &:focus-visible { + @include m.focus-ring(-2px); + } + } + + .validation-warning { + @include w.inline-warning; + margin-top: 6px; + } + } + + // ── Right: Glyph Preview Panel ───────────────────────────────────────────── + + .glyph-preview-panel { + display: flex; + flex-direction: column; + align-items: center; + min-width: 0; + + h3 { + align-self: flex-start; + } + + .preview-svg-container { + width: 100%; + max-width: 260px; + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + margin: v.$space-2 0; + // Let long edge labels render past the drawing box instead of being clipped. + overflow: visible; + + .glyph-preview-svg { + width: 100%; + height: 100%; + overflow: visible; + } + + // SVG user units (viewBox 240): >= 11px on screen once the preview is + // rendered at >= 240px. Names are shortened to MAX_AXIS_LABEL_CHARS; the + // white halo keeps them legible over lines and borders. + .axis-label { + font-size: v.$font-size-xs; + fill: v.$gray-700; + font-family: 'Lucida Sans', 'Lucida Sans Regular', Geneva, Verdana, sans-serif; + paint-order: stroke; + stroke: white; + stroke-width: 3px; + stroke-linejoin: round; + } + } + + .preview-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + max-width: 260px; + aspect-ratio: 1; + color: v.$text-muted; + text-align: center; + + .material-icons { + font-size: v.$icon-size-2xl; + color: v.$gray-400; + margin-bottom: v.$space-2; + } + + p { + font-size: v.$font-size-sm; + margin: 0; + } + } + + .glyph-type-tabs { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: v.$space-1; + + button { + @include w.btn-base; + background: white; + border: 1px solid v.$gray-300; + color: v.$text-muted; + padding: 5px v.$space-3; + font-size: v.$font-size-sm; + gap: v.$space-1; + + &:hover { + background: v.$gray-100; + } + + &.active { + border-color: v.$color-accent; + background: rgba(v.$active-color, 0.08); + color: v.$color-accent; + font-weight: 600; + } + + .material-icons { + font-size: v.$icon-size-sm; + } + } + } + } +} diff --git a/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.spec.ts b/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.spec.ts new file mode 100644 index 0000000..8532c24 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.spec.ts @@ -0,0 +1,677 @@ +import type { Mocked } from 'vitest'; +import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { Step5GlyphColorSettingsComponent } from './step5-glyph-color-settings.component'; +import { WizardAnnouncer } from '../../shared/wizard-announcer.service'; +import { ColorScaleSelectorComponent } from '../../../shared/components/color-scale-selector/color-scale-selector.component'; +import { PreprocessingService } from '../../services/preprocessing.service'; +import { PreprocessingState } from '../../models/preprocessing-state'; +import { ColumnConfig } from '../../models/column-config'; +import { ColumnStatistics, DataProfile } from '../../models/column-statistics'; +import { + DataType, + EncodingMethod, + ScalingMethod, + MissingValueStrategy, + OutlierMethod, + OutlierStrategy, +} from '../../models/data-type.enum'; +import { ToastService } from '../../../services/toast.service'; +import { createTestPreprocessingService, createToastServiceSpy } from '../../testing/preprocessing-test-helpers'; + +function makeConfig(name: string, isColorFeature = false): ColumnConfig { + return { + name, + originalType: DataType.Numeric, + targetType: DataType.Numeric, + encodingMethod: EncodingMethod.Normalize, + scalingMethod: ScalingMethod.MinMax, + includeInProjection: true, + isColorFeature, + missingValueStrategy: MissingValueStrategy.Keep, + outlierMethod: OutlierMethod.IQR_1_5, + outlierStrategy: OutlierStrategy.Keep, + enabled: true, + hasIssues: false, + }; +} + +function setState(service: PreprocessingService, updates: Partial): void { + (service as unknown as { updateState(u: Partial): void }).updateState(updates); +} + +/** The component outside a view: change detection and focus moves are no-ops. */ +function newComponent( + service: PreprocessingService, + toast: ToastService = createToastServiceSpy(), + announcer: WizardAnnouncer = new WizardAnnouncer() +): Step5GlyphColorSettingsComponent { + const cdr = { detectChanges: () => undefined } as unknown as ChangeDetectorRef; + return new Step5GlyphColorSettingsComponent(service, toast, announcer, cdr); +} + +/** Mark loading as finished so glyph/color writes count as genuine user actions. */ +function ready(component: Step5GlyphColorSettingsComponent): void { + (component as unknown as { historyReady: boolean }).historyReady = true; +} + +/** + * Seven numeric columns a…g whose spread grows alphabetically (stdDev 1…7 at a + * common mean), so the smart suggestion is deterministic: g, f, e, d, c. + */ +function seedProfile(service: PreprocessingService, glyphFeatures: string[] = []): void { + const names = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + const columns: ColumnStatistics[] = names.map((name, i) => ({ + name, + dataType: DataType.Numeric, + count: 100, + missingCount: 0, + missingPercentage: 0, + uniqueCount: 50, + mean: 10, + stdDev: i + 1, + })); + const profile: DataProfile = { + totalRows: 100, + totalColumns: names.length, + fileSize: 1000, + fileName: 'test.csv', + columns, + qualityScore: 90, + duplicateCount: 0, + previewRows: [], + }; + setState(service, { + rawFileName: 'test.csv', + dataProfile: profile, + columnConfigs: new Map(names.map(name => [name, makeConfig(name)])), + glyphFeatures, + }); +} + +/** + * A4 – glyph-feature editing vs. the undo history. + * + * Regression coverage for the reported flaky behaviour: deselecting every glyph + * feature one by one and then pressing undo once brought several features back + * instead of exactly one. These tests pin the invariants a correct + * implementation must keep: + * - the local selection never drops below MIN_GLYPH_FEATURES, + * - the local selection and the service state stay in sync after every removal, + * - each persisted removal is exactly one undo entry. + */ +describe('Step5GlyphColorSettingsComponent – glyph feature history', () => { + let component: Step5GlyphColorSettingsComponent; + let service: PreprocessingService; + + const FIVE = ['a', 'b', 'c', 'd', 'e']; + + function startWith(features: string[]): void { + service.setGlyphFeatures([...features]); + component.selectedGlyphFeatures = [...features]; + ready(component); + } + + beforeEach(() => { + localStorage.clear(); + service = createTestPreprocessingService(); + component = newComponent(service); + }); + + afterEach(() => localStorage.clear()); + + it('never lets the selection drop below the minimum when removing repeatedly', () => { + startWith(FIVE); + for (let i = 0; i < 5; i++) { + component.removeGlyphFeature(0); + } + expect(component.selectedGlyphFeatures.length).toBe(component.MIN_GLYPH_FEATURES); + }); + + it('keeps the local selection and the service state in sync after each removal', () => { + startWith(FIVE); + for (let i = 0; i < 5; i++) { + component.removeGlyphFeature(0); + expect(service.currentState.glyphFeatures, `after ${i + 1} removal(s)`).toEqual(component.selectedGlyphFeatures); + } + }); + + it('records exactly one undo entry per persisted removal, so a single undo reverts one step', () => { + startWith(FIVE); + component.removeGlyphFeature(0); + component.removeGlyphFeature(0); + expect(service.currentState.glyphFeatures.length).toBe(3); + + service.undo(); + expect(service.currentState.glyphFeatures.length).toBe(4); + }); + + it('does not push a history entry or change state when removal is blocked at the minimum', () => { + startWith(['a', 'b', 'c']); + expect(service.canUndo).toBe(false); + + component.removeGlyphFeature(0); + + expect(component.selectedGlyphFeatures).toEqual(['a', 'b', 'c']); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c']); + expect(service.canUndo).toBe(false); + }); + + it('adds a glyph feature back as one undoable action', () => { + startWith(['a', 'b', 'c']); + + component.addGlyphFeature('d'); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c', 'd']); + + service.undo(); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c']); + }); +}); + +/** + * A4 – color selection vs. the undo history: a color change is exactly one + * undoable action, while the default color assigned during ngOnInit is not. + */ +describe('Step5GlyphColorSettingsComponent – color feature history', () => { + let component: Step5GlyphColorSettingsComponent; + let service: PreprocessingService; + + /** Two enabled numeric columns; "red" is the initial color feature, scale 0. */ + function seedColorState(): void { + setState(service, { + columnConfigs: new Map([ + ['red', makeConfig('red', true)], + ['blue', makeConfig('blue')], + ]), + colorScaleId: 0, + colorScaleMode: 'continuous', + }); + component.colorFeature = 'red'; + component.selectedColorScaleId = 0; + } + + beforeEach(() => { + localStorage.clear(); + service = createTestPreprocessingService(); + component = newComponent(service); + }); + + afterEach(() => localStorage.clear()); + + it('records a color-feature change as one undoable action', () => { + seedColorState(); + ready(component); + + component.setColorFeature('blue'); + expect(service.currentState.columnConfigs.get('blue')?.isColorFeature).toBe(true); + expect(service.currentState.columnConfigs.get('red')?.isColorFeature).toBe(false); + expect(service.canUndo).toBe(true); + + service.undo(); + expect(service.currentState.columnConfigs.get('red')?.isColorFeature).toBe(true); + expect(service.currentState.columnConfigs.get('blue')?.isColorFeature).toBe(false); + }); + + it('labels a color-feature undo as Color attribute on the Glyphs & Color step', () => { + seedColorState(); + ready(component); + + component.setColorFeature('blue'); + const info = service.undo(); + expect(info?.settingLabel).toBe('Color attribute'); + expect(info?.step).toBe('appearance'); + expect(info?.anchorId).toBe('wizard-anchor-color'); + }); + + it('records a color-scale change as one undoable action', () => { + seedColorState(); + ready(component); + + component.selectColorScale(2); + expect(service.currentState.colorScaleId).toBe(2); + + service.undo(); + expect(service.currentState.colorScaleId).toBe(0); + }); + + it('does not record color history before the step has finished loading', () => { + seedColorState(); + // historyReady stays false, mirroring the ngOnInit default assignment. + component.setColorFeature('blue'); + expect(service.canUndo).toBe(false); + }); +}); + +/** + * A4 – drag & drop and smart suggestions vs. undo. These paths mutate the glyph + * selection; each must be a single undoable action in sync with the service. + */ +describe('Step5GlyphColorSettingsComponent – drag/drop and suggestions', () => { + let component: Step5GlyphColorSettingsComponent; + let service: PreprocessingService; + let toast: Mocked; + + // Minimal DragEvent stand-in: only the members the handlers actually touch. + function dragEvent(): DragEvent { + return { + preventDefault: () => undefined, + stopPropagation: () => undefined, + dataTransfer: { effectAllowed: '', dropEffect: '', setData: () => undefined, getData: () => '' }, + } as unknown as DragEvent; + } + + beforeEach(() => { + localStorage.clear(); + service = createTestPreprocessingService(); + toast = createToastServiceSpy(); + component = newComponent(service, toast); + }); + + afterEach(() => localStorage.clear()); + + it('reorders a glyph feature via drag as one undoable action', () => { + service.setGlyphFeatures(['a', 'b', 'c', 'd', 'e']); + component.selectedGlyphFeatures = ['a', 'b', 'c', 'd', 'e']; + ready(component); + + component.onDragStart(dragEvent(), 'a', 'selected', 0); + component.onDropOnSelectedItem(dragEvent(), 2); + expect(component.selectedGlyphFeatures).toEqual(['b', 'c', 'a', 'd', 'e']); + expect(service.currentState.glyphFeatures).toEqual(['b', 'c', 'a', 'd', 'e']); + + service.undo(); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c', 'd', 'e']); + }); + + it('adds a feature by dropping it into the selected list, undoable', () => { + service.setGlyphFeatures(['a', 'b', 'c']); + component.selectedGlyphFeatures = ['a', 'b', 'c']; + component.availableFeatures = ['a', 'b', 'c', 'd']; + ready(component); + + component.onDragStart(dragEvent(), 'd', 'available', 0); + component.onDropInSelected(dragEvent()); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c', 'd']); + + service.undo(); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c']); + }); + + it('removes a feature by dropping it onto the available area, undoable', () => { + service.setGlyphFeatures(['a', 'b', 'c', 'd']); + component.selectedGlyphFeatures = ['a', 'b', 'c', 'd']; + ready(component); + + component.onDragStart(dragEvent(), 'd', 'selected', 3); + component.onDropInAvailable(dragEvent()); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c']); + + service.undo(); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c', 'd']); + }); + + it('applies recommended features as one undoable action and offers a toast undo', () => { + component.suggestedFeatures = ['a', 'b', 'c', 'd', 'e']; + component.availableFeatures = ['a', 'b', 'c', 'd', 'e', 'f']; + ready(component); + + component.applySuggestedFeaturesByUser(); + expect(component.selectedGlyphFeatures).toEqual(['a', 'b', 'c', 'd', 'e']); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c', 'd', 'e']); + expect(toast.showUndo).toHaveBeenCalled(); + + service.undo(); + expect(service.currentState.glyphFeatures).toEqual([]); + }); + + it('tops short suggestions up to the minimum without duplicates', () => { + component.suggestedFeatures = ['c', 'd']; + component.availableFeatures = ['a', 'b', 'c', 'd']; + + component.applySuggestedFeatures(); + + expect(component.selectedGlyphFeatures).toEqual(['c', 'd', 'a']); + }); + + describe('"Recommended features applied" toast undo is target-specific', () => { + function applyAndGetToastUndo(): () => boolean { + component.suggestedFeatures = ['a', 'b', 'c', 'd', 'e']; + component.availableFeatures = ['a', 'b', 'c', 'd', 'e', 'f']; + ready(component); + component.applySuggestedFeaturesByUser(); + const lastCall = toast.showUndo.mock.lastCall; + if (!lastCall) throw new Error('no undo toast was shown'); + return lastCall[1]; + } + + it('uses English toast text', () => { + applyAndGetToastUndo(); + expect(toast.showUndo.mock.lastCall?.[0]).toBe('Recommended features applied'); + }); + + it('reverts the applied suggestions when nothing happened since', () => { + const toastUndo = applyAndGetToastUndo(); + + expect(toastUndo()).toBe(true); + expect(service.currentState.glyphFeatures).toEqual([]); + }); + + it('does not revert a later, unrelated change', () => { + const toastUndo = applyAndGetToastUndo(); + service.updateCleaningConfig({ removeDuplicates: true }); + + expect(toastUndo()).toBe(false); + expect(service.currentState.cleaningConfig.removeDuplicates).toBe(true); + expect(service.currentState.glyphFeatures).toEqual(['a', 'b', 'c', 'd', 'e']); + }); + + it('offers no toast when nothing was recorded', () => { + component.suggestedFeatures = ['a', 'b', 'c', 'd', 'e']; + component.availableFeatures = ['a', 'b', 'c', 'd', 'e', 'f']; + // historyReady stays false: the write is not a recorded user action. + component.applySuggestedFeaturesByUser(); + + expect(toast.showUndo).not.toHaveBeenCalled(); + }); + }); + + it('warns in English when removing below the minimum', () => { + service.setGlyphFeatures(['a', 'b', 'c']); + component.selectedGlyphFeatures = ['a', 'b', 'c']; + ready(component); + + component.removeGlyphFeature(0); + + expect(toast.warning).toHaveBeenCalledWith('At least 3 glyph features are required.'); + }); +}); + +/** + * Entering the step prefills an empty glyph selection with the recommended + * features without recording an undo entry, and Continue follows the 3–12 rule. + */ +describe('Step5GlyphColorSettingsComponent – prefill and canProceed', () => { + let component: Step5GlyphColorSettingsComponent; + let service: PreprocessingService; + + beforeEach(() => { + localStorage.clear(); + service = createTestPreprocessingService(); + component = newComponent(service); + }); + + afterEach(() => localStorage.clear()); + + it('prefills an empty selection with the recommended features', () => { + seedProfile(service); + + component.ngOnInit(); + + expect(component.selectedGlyphFeatures).toEqual(['g', 'f', 'e', 'd', 'c']); + expect(service.currentState.glyphFeatures).toEqual(['g', 'f', 'e', 'd', 'c']); + }); + + it('creates no undo entry for the prefill or the default color', () => { + seedProfile(service); + + component.ngOnInit(); + + expect(service.canUndo).toBe(false); + expect(component.colorFeature).toBe('g'); + }); + + it('keeps an existing valid selection', () => { + seedProfile(service, ['a', 'b', 'c']); + + component.ngOnInit(); + + expect(component.selectedGlyphFeatures).toEqual(['a', 'b', 'c']); + expect(component.canProceed()).toBe(true); + }); + + it('records later edits after the prefill as undoable actions', () => { + seedProfile(service); + component.ngOnInit(); + + component.addGlyphFeature('a'); + + expect(service.canUndo).toBe(true); + service.undo(); + expect(service.currentState.glyphFeatures).toEqual(['g', 'f', 'e', 'd', 'c']); + }); + + it('is ready to continue right after the prefill', () => { + seedProfile(service); + component.ngOnInit(); + expect(component.canProceed()).toBe(true); + }); + + [ + { count: 2, allowed: false }, + { count: 3, allowed: true }, + { count: 12, allowed: true }, + { count: 13, allowed: false }, + ].forEach(({ count, allowed }) => { + it(`${allowed ? 'allows' : 'blocks'} continuing with ${count} glyph features`, () => { + component.selectedGlyphFeatures = Array.from({ length: count }, (_, i) => `f${i}`); + expect(component.canProceed()).toBe(allowed); + }); + }); + + it('explains the glyph rule when Continue is disabled', () => { + expect(component.disabledHint).toBe('Select 3–12 glyph features to continue.'); + }); + + it('proceeds to the review step', () => { + seedProfile(service); + component.ngOnInit(); + service.goToStep('appearance'); + + component.proceed(); + + expect(service.currentStepId).toBe('review'); + }); +}); + +/** + * Glyph feature order defines the glyph axes. Besides dragging, it can be + * changed with Move up / Move down (WCAG 2.1.1, 2.5.7); each move is one undo + * entry, like a drag, and is announced to screen readers. + */ +describe('Step5GlyphColorSettingsComponent – moving features without dragging', () => { + let component: Step5GlyphColorSettingsComponent; + let service: PreprocessingService; + let announcer: WizardAnnouncer; + let toast: Mocked; + + const FIVE = ['a', 'b', 'c', 'd', 'e']; + + beforeEach(() => { + localStorage.clear(); + service = createTestPreprocessingService(); + announcer = new WizardAnnouncer(); + vi.spyOn(announcer, 'announce').mockImplementation(() => undefined); + toast = createToastServiceSpy(); + component = newComponent(service, toast, announcer); + service.setGlyphFeatures([...FIVE]); + component.selectedGlyphFeatures = [...FIVE]; + ready(component); + }); + + afterEach(() => localStorage.clear()); + + it('moves a feature up as one undoable action and announces its new position', () => { + component.moveGlyphFeature(2, 'up'); + + expect(service.currentState.glyphFeatures).toEqual(['a', 'c', 'b', 'd', 'e']); + expect(announcer.announce).toHaveBeenCalledWith('c moved to position 2 of 5.'); + + service.undo(); + expect(service.currentState.glyphFeatures).toEqual(FIVE); + expect(service.canUndo).toBe(false); + }); + + it('moves a feature down', () => { + component.moveGlyphFeature(0, 'down'); + + expect(component.selectedGlyphFeatures).toEqual(['b', 'a', 'c', 'd', 'e']); + expect(announcer.announce).toHaveBeenCalledWith('a moved to position 2 of 5.'); + }); + + it('does nothing past either end', () => { + component.moveGlyphFeature(0, 'up'); + component.moveGlyphFeature(4, 'down'); + component.moveGlyphFeature(9, 'up'); + + expect(service.currentState.glyphFeatures).toEqual(FIVE); + expect(service.canUndo).toBe(false); + expect(announcer.announce).not.toHaveBeenCalled(); + }); + + it('announces an added feature', () => { + component.availableFeatures = [...FIVE, 'f']; + + component.addAvailableFeature('f', 0); + + expect(service.currentState.glyphFeatures).toEqual([...FIVE, 'f']); + expect(announcer.announce).toHaveBeenCalledWith('f added. 6 glyph features selected.'); + }); + + it('explains instead of silently ignoring an add beyond the maximum', () => { + const twelve = Array.from({ length: 12 }, (_, i) => `f${i}`); + service.setGlyphFeatures(twelve); + component.selectedGlyphFeatures = [...twelve]; + + component.addAvailableFeature('extra', 0); + + expect(component.selectedGlyphFeatures).toEqual(twelve); + expect(toast.warning).toHaveBeenCalledWith('A glyph takes at most 12 features. Remove one first.'); + }); + + it('announces a removed feature, but not a removal blocked at the minimum', () => { + component.removeSelectedFeature(1); + expect(announcer.announce).toHaveBeenCalledWith('b removed. 4 glyph features selected.'); + + component.removeSelectedFeature(0); + vi.mocked(announcer.announce).mockClear(); + component.removeSelectedFeature(0); + + expect(component.selectedGlyphFeatures.length).toBe(3); + expect(announcer.announce).not.toHaveBeenCalled(); + expect(toast.warning).toHaveBeenCalledWith('At least 3 glyph features are required.'); + }); +}); + +describe('Step5GlyphColorSettingsComponent – template', () => { + let service: PreprocessingService; + + beforeEach(async () => { + localStorage.clear(); + service = createTestPreprocessingService(); + seedProfile(service); + await TestBed.configureTestingModule({ + imports: [Step5GlyphColorSettingsComponent], + providers: [ + { provide: PreprocessingService, useValue: service }, + { provide: ToastService, useValue: createToastServiceSpy() }, + ], + }) + .overrideComponent(Step5GlyphColorSettingsComponent, { + set: { imports: [FormsModule, ColorScaleSelectorComponent], schemas: [NO_ERRORS_SCHEMA] }, + }) + .compileComponents(); + }); + + afterEach(() => localStorage.clear()); + + function render(): HTMLElement { + const fixture = TestBed.createComponent(Step5GlyphColorSettingsComponent); + fixture.detectChanges(); + return fixture.nativeElement as HTMLElement; + } + + function selectedNames(el: HTMLElement): (string | undefined)[] { + return Array.from(el.querySelectorAll('.feature-row.selected .feature-name')).map(n => n.textContent?.trim()); + } + + function byLabel(el: HTMLElement, label: string): HTMLButtonElement | null { + return el.querySelector(`button[aria-label="${label}"]`); + } + + it('offers Move up / Move down per selected feature, disabled at the ends', () => { + const el = render(); // prefilled: g, f, e, d, c + + expect(byLabel(el, 'Move g up')?.disabled).toBe(true); + expect(byLabel(el, 'Move g down')?.disabled).toBe(false); + expect(byLabel(el, 'Move c up')?.disabled).toBe(false); + expect(byLabel(el, 'Move c down')?.disabled).toBe(true); + expect(byLabel(el, 'Remove e')).not.toBeNull(); + }); + + it('moves a feature with its button and keeps focus on the moved feature', () => { + const el = render(); + + byLabel(el, 'Move e up')?.click(); + expect(selectedNames(el)).toEqual(['g', 'e', 'f', 'd', 'c']); + expect(document.activeElement?.getAttribute('aria-label')).toBe('Move e up'); + + byLabel(el, 'Move e up')?.click(); + expect(selectedNames(el)).toEqual(['e', 'g', 'f', 'd', 'c']); + // "Move up" is disabled at the top, so focus switches to "Move down". + expect(document.activeElement?.getAttribute('aria-label')).toBe('Move e down'); + }); + + it('renders available features as buttons named "Add "', () => { + const el = render(); + const buttons = Array.from(el.querySelectorAll('button.feature-row.available')); + + expect(buttons.map(b => b.getAttribute('aria-label'))).toEqual(['Add a', 'Add b']); + + buttons[0].click(); + expect(selectedNames(el)).toEqual(['g', 'f', 'e', 'd', 'c', 'a']); + expect(document.activeElement?.getAttribute('aria-label')).toBe('Add b'); + }); + + it('keeps help buttons out of the label and the heading', () => { + const el = render(); + + const label = el.querySelector('label[for="step5-color-attribute"]'); + expect(label?.textContent?.trim()).toBe('Color attribute'); + expect(label?.querySelector('app-help-tooltip, button')).toBeNull(); + expect(el.querySelector('#step5-glyph-heading app-help-tooltip')).toBeNull(); + }); + + it('names the color scale picker by its visible label', () => { + const el = render(); + const trigger = el.querySelector('app-color-scale-selector button.selected-scale'); + + expect(trigger?.getAttribute('aria-labelledby')?.split(' ')[0]).toBe('step5-color-scale-label'); + expect(el.querySelector('#step5-color-scale-label')?.textContent?.trim()).toBe('Color scale'); + }); + + it('marks the active glyph type with aria-pressed', () => { + const pressed = Array.from(render().querySelectorAll('.glyph-type-tabs button')).map(b => + b.getAttribute('aria-pressed') + ); + expect(pressed).toEqual(['true', 'false', 'false']); + }); + + it('tells the user these settings can be changed later in the dashboard', () => { + const text = (render().textContent ?? '').replace(/\s+/g, ' '); + expect(text).toContain("You can change these later in the dashboard's Encoding panel."); + }); + + it('keeps the deep-link anchors for color and glyph features', () => { + const el = render(); + expect(el.querySelector('#wizard-anchor-color')).not.toBeNull(); + expect(el.querySelector('#wizard-anchor-glyph')).not.toBeNull(); + }); + + it('renders the prefilled features in the selected list', () => { + const names = Array.from(render().querySelectorAll('.feature-row.selected .feature-name')).map(n => + n.textContent?.trim() + ); + expect(names).toEqual(['g', 'f', 'e', 'd', 'c']); + }); +}); diff --git a/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.ts b/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.ts new file mode 100644 index 0000000..9ed1c47 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step5-glyph-color-settings/step5-glyph-color-settings.component.ts @@ -0,0 +1,519 @@ +import { ChangeDetectorRef, Component, OnInit, forwardRef } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { PreprocessingService } from '../../services/preprocessing.service'; +import { WizardStep, WIZARD_STEP } from '../../shared/wizard-step'; +import { WizardAnnouncer } from '../../shared/wizard-announcer.service'; +import { ColumnStatistics } from '../../models/column-statistics'; +import { DataType } from '../../models/data-type.enum'; +import { HelpTooltipComponent } from '../../shared/help-tooltip/help-tooltip.component'; +import { HELP_TEXT } from '../../shared/constants/help-text'; +import { ColorScale, buildGroupedColorScales, getColorScaleById } from '../../../shared/interfaces/color-scale'; +import { ColorScaleSelectorComponent } from '../../../shared/components/color-scale-selector/color-scale-selector.component'; +import { ToastService } from '../../../services/toast.service'; +import { GlyphPreview, PREVIEW_CENTER, PREVIEW_RADIUS, buildGlyphPreview } from './glyph-preview-geometry'; + +export const MIN_GLYPH_FEATURES = 3; +export const MAX_GLYPH_FEATURES = 12; +/** How many top-scored features the smart suggestion picks. */ +const SUGGESTED_FEATURE_COUNT = 5; +/** Upper bound of distinct values for a categorical column to make a legible default color. */ +const MAX_DEFAULT_COLOR_CATEGORIES = 12; + +type FeatureList = 'selected' | 'available'; +export type MoveDirection = 'up' | 'down'; + +/** + * Step 5 – Glyphs & Color: color attribute + scale and the ordered glyph + * features. This only configures the dashboard's initial view (it is written + * into the schema's default context), so the step prefills recommended features + * and lets the user continue as soon as the glyph selection is valid. + */ +@Component({ + selector: 'app-step5-glyph-color-settings', + standalone: true, + imports: [FormsModule, HelpTooltipComponent, ColorScaleSelectorComponent], + templateUrl: './step5-glyph-color-settings.component.html', + styleUrl: './step5-glyph-color-settings.component.scss', + providers: [{ provide: WIZARD_STEP, useExisting: forwardRef(() => Step5GlyphColorSettingsComponent) }], +}) +export class Step5GlyphColorSettingsComponent implements OnInit, WizardStep { + readonly primaryLabel = 'Continue to Review'; + readonly disabledHint = `Select ${MIN_GLYPH_FEATURES}–${MAX_GLYPH_FEATURES} glyph features to continue.`; + + // Color feature selection + columns: ColumnStatistics[] = []; + colorFeature: string | null = null; + selectedColorScaleId = 0; + groupedColorScales: { group: string; scales: ColorScale[] }[] = []; + + // Glyph feature mapping + availableFeatures: string[] = []; + selectedGlyphFeatures: string[] = []; + suggestedFeatures: string[] = []; + featureVariances = new Map(); + readonly MIN_GLYPH_FEATURES = MIN_GLYPH_FEATURES; + readonly MAX_GLYPH_FEATURES = MAX_GLYPH_FEATURES; + draggedFeature: string | null = null; + draggedFromList: FeatureList = 'available'; + draggedIndex = -1; + // Index of the selected row currently under the drag cursor (insertion highlight). + dragOverIndex = -1; + + // Search + type filter for the AVAILABLE feature list. + featureSearch = ''; + featureTypeFilter: DataType | 'all' = 'all'; + readonly DataType = DataType; + + // Glyph preview + selectedGlyphType: 'star' | 'flower' | 'whisker' = 'star'; + preview: GlyphPreview = buildGlyphPreview([]); + readonly PREVIEW_RADIUS = PREVIEW_RADIUS; + readonly PREVIEW_CENTER = PREVIEW_CENTER; + + readonly HELP_TEXT = HELP_TEXT; + + // A4: guards the history pushes so writes during the initial load (the + // prefill and the default color) do not create undo entries. Flipped true once + // the step has finished loading. + private historyReady = false; + + constructor( + public preprocessingService: PreprocessingService, + private toastService: ToastService, + private announcer: WizardAnnouncer, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit(): void { + const state = this.preprocessingService.currentState; + this.columns = (state.dataProfile?.columns ?? []).filter(col => state.columnConfigs.get(col.name)?.enabled); + + this.initColorFeature(); + this.groupedColorScales = buildGroupedColorScales(); + + this.updateAvailableFeatures(); + this.calculateSmartSuggestions(); + // Prefill: an empty (or too small) selection gets the recommended features. + // historyReady is still false, so this records no undo entry. + if (state.glyphFeatures.length >= MIN_GLYPH_FEATURES) { + this.selectedGlyphFeatures = [...state.glyphFeatures]; + this.regeneratePreviewData(); + } else { + this.applySuggestedFeatures(); + } + + this.historyReady = true; + } + + // ============================================================================ + // Color Feature Selection + // ============================================================================ + + /** Adopt the stored color attribute, or assign a smart default (no undo entry). */ + private initColorFeature(): void { + const stored = Array.from(this.preprocessingService.currentState.columnConfigs.values()).find( + c => c.isColorFeature + ); + this.colorFeature = stored?.name ?? this.pickDefaultColorFeature(); + if (!stored && this.colorFeature) { + this.preprocessingService.setColorFeature(this.colorFeature); + } + // Read after setColorFeature, which may auto-switch the scale to match the type. + this.selectedColorScaleId = this.preprocessingService.currentState.colorScaleId; + } + + setColorFeature(columnName: string): void { + // A4: record the color change so undo reverts the color feature (and any + // auto-switched scale) as one action. + if (this.historyReady) { + this.preprocessingService.pushHistory('Color attribute changed'); + } + this.colorFeature = columnName; + this.preprocessingService.setColorFeature(columnName); + // Sync selected scale ID after service auto-switches on type mismatch + this.selectedColorScaleId = this.preprocessingService.currentState.colorScaleId; + } + + /** + * Smart default for the color attribute. Prefers a low-cardinality + * categorical/boolean column (distinct, legible color groups), then falls + * back to the numeric column with the most spread (a meaningful gradient), + * and only as a last resort to the first column. Returns null if there are + * no columns. + */ + private pickDefaultColorFeature(): string | null { + if (this.columns.length === 0) return null; + + const lowCardCategorical = this.columns + .filter( + c => + (c.dataType === DataType.Categorical || c.dataType === DataType.Boolean) && + c.uniqueCount >= 2 && + c.uniqueCount <= MAX_DEFAULT_COLOR_CATEGORIES + ) + .sort((a, b) => a.uniqueCount - b.uniqueCount)[0]; + if (lowCardCategorical) return lowCardCategorical.name; + + const variance = (c: ColumnStatistics): number => c.variance ?? (c.stdDev ?? 0) ** 2; + const numericBySpread = this.columns + .filter(c => c.dataType === DataType.Numeric && variance(c) > 0) + .sort((a, b) => variance(b) - variance(a))[0]; + if (numericBySpread) return numericBySpread.name; + + return this.columns[0].name; + } + + getSelectedColorScale(): ColorScale { + return getColorScaleById(this.selectedColorScaleId); + } + + selectColorScale(id: number): void { + // A4: same as the color feature — record so a scale change is undoable. + if (this.historyReady) { + this.preprocessingService.pushHistory('Color scale changed'); + } + this.selectedColorScaleId = id; + this.preprocessingService.setColorScaleId(id); + } + + // ============================================================================ + // Glyph Feature Mapping + // ============================================================================ + + updateAvailableFeatures(): void { + this.availableFeatures = this.preprocessingService.getPreviewFeatureNames(); + } + + /** Score features by relative spread and keep the top ones as suggestions. */ + calculateSmartSuggestions(): void { + const profile = this.preprocessingService.currentState.dataProfile; + if (!profile) return; + + const scores = this.availableFeatures.flatMap(feature => { + const colStats = this.findColumnStats(feature); + return colStats ? [{ name: feature, score: variationScore(colStats) }] : []; + }); + + this.featureVariances = new Map(scores.map(({ name, score }) => [name, score])); + this.suggestedFeatures = [...scores] + .sort((a, b) => b.score - a.score) + .slice(0, SUGGESTED_FEATURE_COUNT) + .map(f => f.name); + } + + suggestionsWouldChange(): boolean { + if (this.suggestedFeatures.length === 0) return false; + const target = this.suggestedFeatures.slice(0, SUGGESTED_FEATURE_COUNT); + if (target.length !== this.selectedGlyphFeatures.length) return true; + return !target.every((f, i) => f === this.selectedGlyphFeatures[i]); + } + + /** Select the suggested features; returns the recorded undo entry id, or null. */ + applySuggestedFeatures(): number | null { + if (this.suggestedFeatures.length === 0) { + this.calculateSmartSuggestions(); + } + const suggested = this.suggestedFeatures.slice(0, SUGGESTED_FEATURE_COUNT); + // Top up to the minimum with other available features (never duplicates). + const extras = this.availableFeatures.filter(feature => !suggested.includes(feature)); + const next = [...suggested, ...extras].slice(0, Math.max(suggested.length, MIN_GLYPH_FEATURES)); + return this.commitSelection(next); + } + + /** + * A4: user-triggered "apply recommended features" (Smart Defaults). Applies the + * suggestions (which records a single history entry via saveGlyphFeatures) and + * shows a confirmation toast whose undo reverts exactly that entry. + */ + applySuggestedFeaturesByUser(): void { + const entryId = this.applySuggestedFeatures(); + if (entryId !== null) { + this.toastService.showUndo('Recommended features applied', () => this.preprocessingService.undoIfLatest(entryId)); + } + } + + addGlyphFeature(feature: string): void { + this.insertSelectedAt(feature, this.selectedGlyphFeatures.length); + } + + removeGlyphFeature(index: number): void { + // A glyph needs at least MIN_GLYPH_FEATURES rays. Removing below that is not a + // persistable state (setGlyphFeatures rejects it), so block it here instead of + // letting the local selection drift out of sync with the service and skipping + // the history entry — that mismatch is what made undo restore several features. + if (this.selectedGlyphFeatures.length <= MIN_GLYPH_FEATURES) { + this.toastService.warning(`At least ${MIN_GLYPH_FEATURES} glyph features are required.`); + return; + } + this.commitSelection(this.selectedGlyphFeatures.filter((_, i) => i !== index)); + } + + isFeatureSelected(feature: string): boolean { + return this.selectedGlyphFeatures.includes(feature); + } + + /** Data type of a feature, resolving one-hot columns (e.g. city_NYC → city). */ + getFeatureType(feature: string): DataType | null { + return this.findColumnStats(feature)?.dataType ?? null; + } + + /** Available features minus the already-selected ones, filtered by search + type. */ + get filteredAvailableFeatures(): string[] { + const term = this.featureSearch.trim().toLowerCase(); + return this.availableFeatures.filter(feature => { + if (this.isFeatureSelected(feature)) return false; + if (term && !feature.toLowerCase().includes(term)) return false; + return this.featureTypeFilter === 'all' || this.getFeatureType(feature) === this.featureTypeFilter; + }); + } + + clearFeatureFilters(): void { + this.featureSearch = ''; + this.featureTypeFilter = 'all'; + } + + getFeatureVariance(feature: string): number | null { + return this.featureVariances.get(feature) ?? null; + } + + getFeatureVariancePercent(feature: string): number { + const val = this.featureVariances.get(feature); + if (val === undefined) return 0; + const maxVar = Math.max(...this.featureVariances.values()); + return maxVar > 0 ? (val / maxVar) * 100 : 0; + } + + /** True while the glyph selection is in the valid 3–12 range. */ + get isGlyphSelectionValid(): boolean { + const count = this.selectedGlyphFeatures.length; + return count >= MIN_GLYPH_FEATURES && count <= MAX_GLYPH_FEATURES; + } + + /** + * Persist the local glyph selection. Returns the id of the recorded undo entry, + * or null when nothing was recorded (invalid count or still loading). + */ + saveGlyphFeatures(): number | null { + if (!this.isGlyphSelectionValid) { + return null; + } + // A4: every completed glyph edit (add/remove/reorder/apply-suggestions) + // funnels through here, so a single history push covers them all. Snapshot + // the state before the write so undo restores the previous feature set. + const entryId = this.historyReady ? this.preprocessingService.pushHistory('Glyph features changed') : null; + this.preprocessingService.setGlyphFeatures(this.selectedGlyphFeatures); + return entryId; + } + + /** Adopt a new selection, persist it (one undo entry) and refresh the preview. */ + private commitSelection(next: string[]): number | null { + this.selectedGlyphFeatures = next; + const entryId = this.saveGlyphFeatures(); + this.regeneratePreviewData(); + return entryId; + } + + private findColumnStats(feature: string): ColumnStatistics | undefined { + const columns = this.preprocessingService.currentState.dataProfile?.columns ?? []; + // Exact match first, then the source column of a one-hot feature (city_NYC → city). + return columns.find(c => c.name === feature) ?? columns.find(c => feature.startsWith(c.name + '_')); + } + + // ============================================================================ + // Drag & Drop + // ============================================================================ + + onDragStart(event: DragEvent, feature: string, fromList: FeatureList, index: number): void { + this.draggedFeature = feature; + this.draggedFromList = fromList; + this.draggedIndex = index; + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', feature); + } + } + + onDragEnd(_event: DragEvent): void { + this.draggedFeature = null; + this.draggedFromList = 'available'; + this.draggedIndex = -1; + this.dragOverIndex = -1; + } + + onDragOver(event: DragEvent): void { + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + } + + /** Hovering a specific selected row while dragging — shows the insertion point. */ + onItemDragOver(event: DragEvent, index: number): void { + event.stopPropagation(); + this.onDragOver(event); + this.dragOverIndex = index; + } + + /** Insert a feature into the selected list at a specific position. */ + private insertSelectedAt(feature: string, index: number): void { + if (this.selectedGlyphFeatures.length >= MAX_GLYPH_FEATURES || this.isFeatureSelected(feature)) return; + const at = Math.max(0, Math.min(index, this.selectedGlyphFeatures.length)); + this.commitSelection([ + ...this.selectedGlyphFeatures.slice(0, at), + feature, + ...this.selectedGlyphFeatures.slice(at), + ]); + } + + /** Move a selected feature to a new position (reordering — order drives the glyph). */ + private moveSelected(from: number, to: number): void { + const current = this.selectedGlyphFeatures; + if (from < 0 || from >= current.length) return; + const target = Math.max(0, Math.min(to, current.length - 1)); + if (from === target) return; + const rest = current.filter((_, i) => i !== from); + this.commitSelection([...rest.slice(0, target), current[from], ...rest.slice(target)]); + } + + /** Drop onto a specific selected row: reorder (from selected) or insert (from available). */ + onDropOnSelectedItem(event: DragEvent, targetIndex: number): void { + event.preventDefault(); + event.stopPropagation(); + if (this.draggedFeature && this.draggedFromList === 'selected') { + this.moveSelected(this.draggedIndex, targetIndex); + } else if (this.draggedFeature) { + this.insertSelectedAt(this.draggedFeature, targetIndex); + } + this.onDragEnd(event); + } + + /** Drop on the selected list background: append (available) or move to end (selected). */ + onDropInSelected(event: DragEvent): void { + event.preventDefault(); + if (!this.draggedFeature) return; + + if (this.draggedFromList === 'available') { + this.addGlyphFeature(this.draggedFeature); + } else { + this.moveSelected(this.draggedIndex, this.selectedGlyphFeatures.length - 1); + } + this.onDragEnd(event); + } + + /** Drop a selected feature onto the available area to remove it from the glyph. */ + onDropInAvailable(event: DragEvent): void { + event.preventDefault(); + if (this.draggedFeature && this.draggedFromList === 'selected') { + const idx = this.selectedGlyphFeatures.indexOf(this.draggedFeature); + if (idx !== -1) { + this.removeGlyphFeature(idx); + } + } + this.onDragEnd(event); + } + + // ============================================================================ + // Keyboard / button alternatives to drag & drop (WCAG 2.1.1, 2.5.7) + // ============================================================================ + + moveButtonId(direction: MoveDirection, index: number): string { + return `step5-move-${direction}-${index}`; + } + + removeButtonId(index: number): string { + return `step5-remove-${index}`; + } + + availableButtonId(index: number): string { + return `step5-available-${index}`; + } + + /** + * Move a selected feature one position up or down — one undo entry, like a + * drag. Focus follows the feature to its new row; once it reaches an end, the + * button that still works there (the other direction) takes focus. + */ + moveGlyphFeature(index: number, direction: MoveDirection): void { + const feature = this.selectedGlyphFeatures[index]; + const target = direction === 'up' ? index - 1 : index + 1; + const total = this.selectedGlyphFeatures.length; + if (feature === undefined || target < 0 || target >= total) return; + + this.moveSelected(index, target); + this.announcer.announce(`${feature} moved to position ${target + 1} of ${total}.`); + + const atEnd = direction === 'up' ? target === 0 : target === total - 1; + const opposite: MoveDirection = direction === 'up' ? 'down' : 'up'; + this.focusElement(this.moveButtonId(atEnd ? opposite : direction, target)); + } + + /** Add a feature from the available list; focus stays in that list. */ + addAvailableFeature(feature: string, index: number): void { + if (this.selectedGlyphFeatures.length >= MAX_GLYPH_FEATURES) { + this.toastService.warning(`A glyph takes at most ${MAX_GLYPH_FEATURES} features. Remove one first.`); + return; + } + this.addGlyphFeature(feature); + const total = this.selectedGlyphFeatures.length; + this.announcer.announce(`${feature} added. ${total} glyph features selected.`); + + const remaining = this.filteredAvailableFeatures.length; + this.focusElement( + remaining > 0 ? this.availableButtonId(Math.min(index, remaining - 1)) : this.removeButtonId(total - 1) + ); + } + + /** Remove a selected feature; focus moves to the row that takes its place. */ + removeSelectedFeature(index: number): void { + const feature = this.selectedGlyphFeatures[index]; + const before = this.selectedGlyphFeatures.length; + this.removeGlyphFeature(index); + const total = this.selectedGlyphFeatures.length; + // Blocked at the minimum: the warning toast explains why, focus stays put. + if (feature === undefined || total === before) return; + + this.announcer.announce(`${feature} removed. ${total} glyph features selected.`); + this.focusElement(this.removeButtonId(Math.min(index, total - 1))); + } + + /** Render the new order first, then move focus (the pressed button may have moved or gone). */ + private focusElement(id: string): void { + this.cdr.detectChanges(); + document.getElementById(id)?.focus(); + } + + // ============================================================================ + // Glyph Preview (SVG) + // ============================================================================ + + /** Text alternative of the preview: glyph type and its axes in order. */ + get previewDescription(): string { + return `${this.selectedGlyphType} glyph preview with axes: ${this.selectedGlyphFeatures.join(', ')}`; + } + + private regeneratePreviewData(): void { + this.preview = buildGlyphPreview(this.selectedGlyphFeatures); + } + + // ============================================================================ + // Navigation + // ============================================================================ + + canProceed(): boolean { + return this.isGlyphSelectionValid; + } + + proceed(): void { + if (this.canProceed()) { + this.preprocessingService.nextStep(); + } + } +} + +/** Relative spread of a column: coefficient of variation, else the share of unique values. */ +function variationScore(stats: ColumnStatistics): number { + if (stats.stdDev !== undefined && stats.mean !== undefined && Math.abs(stats.mean) > 0) { + return stats.stdDev / Math.abs(stats.mean); + } + return stats.uniqueCount / stats.count; +} diff --git a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html deleted file mode 100644 index c51425a..0000000 --- a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html +++ /dev/null @@ -1,395 +0,0 @@ -
    -
    - - @if (showProcessing) { -
    - save - Your work is saved -
    - } - - - - - @if (!showProcessing) { -
    - -
    -
    -
    view_column
    -
    - Columns Selected - {{ enabledColumns }} / {{ totalColumns }} -
    - -
    - -
    -
    tune
    -
    - Projection Features - {{ projectionColumns }} -
    - -
    - -
    -
    palette
    -
    - Color Feature - {{ colorFeature || 'None' }} -
    - -
    - -
    -
    star_outline
    -
    - Glyph Features - {{ selectedGlyphFeatures.length }} -
    - -
    - -
    -
    settings
    -
    - Projection Methods - {{ enabledMethods.join(', ') }} -
    - -
    -
    - - - @if (preflightIssues.length > 0) { -
    - info -
    - Heads up — {{ preflightIssues.length }} - {{ preflightIssues.length === 1 ? 'thing' : 'things' }} could cause processing to fail. You can start - anyway, or fix them first. -
    -
    -
    - @for (issue of preflightIssues; track issue.code + issue.title) { - - } -
    - } - - -
    - - - - - - - - - - - - @for (config of columnConfigs; track config.name) { - @if (config.enabled) { - - - - - - - - } - } - -
    ColumnTypeEncodingScalingIn Projection
    - {{ config.name }} - @if (config.isColorFeature) { - - palette - - } - - - {{ getEncodingLabel(config.encodingMethod) }} - @if (config.targetType === DataType.Numeric || config.targetType === DataType.Date) { - {{ getScalingLabel(config.scalingMethod) }} - } @else { - N/A - } - - @if (config.includeInProjection) { - - check_circle - Yes - - } @else { - - cancel - No - - } -
    -
    - - -
    - info -
    - Processing Info: Processing time depends on your dataset size and enabled projection - methods. FastMap loads immediately, allowing you to explore your data right away. Additional projections - compute in the background. If anything fails, it appears here with a fix — never a blank screen. -
    -
    -
    - } - - - - - @if (showProcessing) { - - @if (isProcessing) { -
    -
    - sync -
    -

    {{ processingStep || 'FastMap is computing your primary layout…' }}

    -

    - FastMap loads first so you can explore right away. Other projections keep computing in the background — - if a problem comes up, it shows here, not on a blank page. -

    -
    -
    - -
    -
    -
    -
    {{ processingProgress }}%
    - - -
    - } - - - @if (isErrorScreen && blockingIssue) { -
    -
    - error_outline -
    -

    We couldn't finish your visualization — but this is fixable.

    -

    Your uploaded data and all settings are still there — nothing was lost.

    -
    -
    - - - -
    - -
    -
    - } - - - @if (processingComplete && !isProcessing) { -
    -
    - check_circle -
    -

    Your visualization is ready

    -

    - FastMap is loaded — open Glyphspace to start exploring. Additional projections finish in the background. -

    -
    -
    - - @if (isPartialResult) { -
    - warning -
    - {{ partialIssues.length }} of {{ enabledMethods.length }} projections could not be computed. Your - visualization still works with the ones that succeeded. -
    -
    -
    - @for (issue of partialIssues; track issue.title) { - - } -
    - } @else { -
    -
    - check{{ enabledColumns }} columns processed -
    -
    - checkFastMap projection ready -
    -
    checkData cleaning applied
    -
    - } - - - - - @if (getBackgroundProjectionsArray().length > 0) { -
    -

    - schedule - Background Projections -

    -
    - @for (proj of getBackgroundProjectionsArray(); track proj.method) { -
    -
    - {{ proj.method.toUpperCase() }} - @if (proj.status === 'running') { - syncRunning - } - @if (proj.status === 'complete') { - check_circleComplete - } - @if (proj.status === 'error') { - errorFailed - } -
    - @if (proj.status === 'running') { -
    -
    -
    -

    {{ proj.message }}

    - } - @if (proj.status === 'complete') { -

    Available in Glyphspace dropdown

    - } -
    - } -
    -
    - } -
    - } - } -
    - - - -
    - @for (m of methodStatuses; track m.name) { -
    -
    - {{ m.name }} - {{ m.role }} -
    - {{ m.statusLabel }} -
    - } -
    -
    - - - @if (!showProcessing) { -
    - -
    - -
    -
    - } - - @if (isErrorScreen) { -
    - -
    - -
    -
    - } - - @if (processingComplete && !isProcessing) { -
    - -
    - - -
    -
    - } -
    diff --git a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss deleted file mode 100644 index b21647a..0000000 --- a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss +++ /dev/null @@ -1,660 +0,0 @@ -@use 'sass:color'; -@use '../../shared/wizard-shared' as w; -@use '../../../../styles/variables' as v; - -.step5-review-processing { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - - .step-content { - @include w.step-content; - } - - .step-header { - @include w.step-header; - } - - // Review Section - .review-section { - .summary-cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 12px; - margin-bottom: 16px; - - .summary-card { - display: flex; - gap: 12px; - padding: 14px; - background: white; - border: 1px solid v.$gray-300; - border-radius: 4px; - - // A6: edit/jump affordance per summary card — uniform, borderless icon button - .card-edit-btn { - margin-left: auto; - align-self: flex-start; - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - padding: 0; - background: transparent; - border: none; - border-radius: 4px; - color: v.$gray-500; - cursor: pointer; - transition: - color 0.15s ease, - background 0.15s ease; - - .material-icons { - font-size: 18px; - } - - &:hover { - color: v.$status-info; - background: rgba(v.$active-color, 0.1); - } - - &:focus-visible { - outline: 2px solid v.$active-color; - outline-offset: 1px; - } - } - - .card-icon { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: rgba(v.$active-color, 0.1); - border-radius: 50%; - - .material-icons { - font-size: 22px; - color: v.$active-color; - } - } - - .card-content { - display: flex; - flex-direction: column; - gap: 2px; - - .card-label { - font-size: 11px; - font-weight: 600; - color: v.$gray-600; - text-transform: uppercase; - } - - .card-value { - font-size: 16px; - font-weight: 700; - color: v.$gray-800; - } - } - } - } - - .columns-table { - @include w.table-container; - margin-bottom: 16px; - - table { - @include w.table; - } - - .column-name-text { - @include w.column-name-text; - } - - .color-badge { - display: inline-flex; - align-items: center; - margin-left: 6px; - - .material-icons { - font-size: 14px; - color: v.$active-color; - } - } - - .data-type-badge { - @include w.badge; - } - - .text-muted { - font-size: 12px; - color: v.$gray-500; - font-style: italic; - } - - .status-badge { - @include w.status-badge; - } - } - - .info-box { - @include w.info-box; - } - } - - // Processing Section - .processing-view, - .success-view { - display: flex; - align-items: center; - justify-content: center; - min-height: 400px; - - .processing-card, - .success-card { - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; - padding: 40px; - background: white; - border: 1px solid v.$gray-300; - border-radius: 8px; - max-width: 600px; - text-align: center; - - .processing-icon, - .success-icon { - display: flex; - align-items: center; - justify-content: center; - width: 80px; - height: 80px; - background: rgba(v.$active-color, 0.1); - border-radius: 50%; - - .material-icons { - font-size: 48px; - - &.spinning { - color: v.$active-color; - animation: spin 1s linear infinite; - } - } - } - - .success-icon { - background: rgba(v.$status-success, 0.1); - - .material-icons { - color: v.$status-success; - } - } - - h3 { - font-size: 20px; - font-weight: 700; - color: v.$gray-800; - margin: 0; - } - - .processing-message, - .success-message { - font-size: 14px; - color: v.$gray-600; - } - - .progress-bar { - @include w.progress-bar; - width: 100%; - max-width: 400px; - height: 8px; - } - - .progress-fill { - @include w.progress-fill; - } - - .progress-text { - font-size: 16px; - font-weight: 700; - color: v.$active-color; - } - - .success-stats { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - - .stat-item { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - font-size: 14px; - color: v.$gray-700; - - .material-icons { - font-size: 18px; - color: v.$status-success; - } - } - } - - .background-status-section { - width: 100%; - padding-top: 16px; - border-top: 1px solid v.$gray-300; - - h4 { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - font-size: 14px; - font-weight: 600; - color: v.$gray-700; - margin-bottom: 12px; - - .material-icons { - font-size: 18px; - } - } - - .projection-status-list { - display: flex; - flex-direction: column; - gap: 10px; - - .projection-status-item { - padding: 10px; - background: v.$gray-100; - border-radius: 4px; - - .status-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 6px; - - .method-name { - font-size: 13px; - font-weight: 600; - color: v.$gray-800; - } - - .status-badge { - @include w.status-badge; - } - } - - .progress-bar-small { - @include w.progress-bar; - height: 4px; - margin-bottom: 4px; - - .progress-fill-small { - @include w.progress-fill; - } - } - - .status-message { - font-size: 12px; - color: v.$gray-600; - margin: 0; - } - } - } - } - } - } - - // A3: persistence signifier pill - .persistence-pill { - display: inline-flex; - align-items: center; - gap: 6px; - align-self: flex-start; - margin-bottom: 14px; - padding: 4px 10px; - background: rgba(v.$active-color, 0.1); - border: 1px solid rgba(v.$active-color, 0.4); - border-radius: 12px; - font-size: 12px; - font-weight: 600; - color: v.$status-info; - - .material-icons { - font-size: 15px; - } - } - - // A3: pre-flight banner (amber) - .preflight-banner { - display: flex; - gap: 10px; - align-items: flex-start; - padding: 12px 14px; - margin-bottom: 12px; - background: #fffdf9; - border: 1px solid #f2dcb6; - border-radius: 6px; - font-size: 13px; - color: v.$gray-700; - line-height: 1.45; - - .material-icons { - color: #d08a1e; - font-size: 20px; - flex-shrink: 0; - } - } - - .issue-list { - display: flex; - flex-direction: column; - gap: 12px; - margin-bottom: 16px; - } - - // A3: persistent status/error/result panel - .status-panel { - display: flex; - flex-direction: column; - gap: 16px; - padding: 20px; - background: white; - border: 1px solid v.$gray-300; - border-radius: 8px; - - .status-hero { - display: flex; - align-items: center; - gap: 16px; - padding: 16px; - border-radius: 6px; - - .material-icons { - font-size: 40px; - flex-shrink: 0; - - &.spinning { - animation: spin 1s linear infinite; - } - } - - h3 { - font-size: 17px; - font-weight: 700; - margin: 0 0 4px; - color: v.$gray-800; - } - - p { - font-size: 13px; - color: v.$gray-600; - margin: 0; - line-height: 1.45; - } - - &.processing { - background: rgba(v.$active-color, 0.08); - .material-icons { - color: v.$active-color; - } - } - - &.error { - background: #fffafa; - border: 1px solid #f0d3cf; - .material-icons { - color: #d64541; - } - } - } - - .success-banner { - display: flex; - align-items: center; - gap: 16px; - padding: 16px; - background: #f1faf2; - border: 1px solid #c8ead0; - border-radius: 6px; - - .material-icons { - font-size: 40px; - color: #2a9d4a; - flex-shrink: 0; - } - - h3 { - font-size: 17px; - font-weight: 700; - margin: 0 0 4px; - color: v.$gray-800; - } - - p { - font-size: 13px; - color: v.$gray-600; - margin: 0; - line-height: 1.45; - } - } - - .partial-notice { - display: flex; - gap: 10px; - align-items: flex-start; - padding: 12px 14px; - background: #fffdf9; - border: 1px solid #f2dcb6; - border-radius: 6px; - font-size: 13px; - color: v.$gray-700; - line-height: 1.45; - - .material-icons { - color: #d08a1e; - font-size: 20px; - flex-shrink: 0; - } - } - - .progress-bar { - @include w.progress-bar; - width: 100%; - height: 8px; - } - - .progress-fill { - @include w.progress-fill; - } - - .progress-text { - font-size: 16px; - font-weight: 700; - color: v.$active-color; - text-align: center; - } - - .success-stats { - display: flex; - flex-direction: column; - gap: 8px; - - .stat-item { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - color: v.$gray-700; - - .material-icons { - font-size: 18px; - color: #2a9d4a; - } - } - } - - // Method status bar - .method-bar { - display: flex; - flex-wrap: wrap; - gap: 10px; - - .method-chip { - flex: 1 1 140px; - display: flex; - flex-direction: column; - gap: 4px; - padding: 10px 12px; - border: 1px solid v.$gray-200; - border-radius: 6px; - background: v.$gray-100; - - .chip-top { - display: flex; - align-items: baseline; - gap: 6px; - - .chip-name { - font-size: 13px; - font-weight: 700; - color: v.$gray-800; - } - - .chip-role { - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.03em; - color: v.$gray-500; - } - } - - .chip-status { - font-size: 12px; - font-weight: 600; - color: v.$gray-600; - } - - &.chip-ready { - background: #f1faf2; - border-color: #c8ead0; - .chip-status { - color: #2a9d4a; - } - } - - &.chip-computing { - background: rgba(v.$active-color, 0.08); - border-color: rgba(v.$active-color, 0.35); - .chip-status { - color: v.$status-info; - } - } - - &.chip-failed { - background: #fffafa; - border-color: #f0d3cf; - .chip-status { - color: #d64541; - } - } - } - } - - .background-status-section { - padding-top: 8px; - border-top: 1px solid v.$gray-200; - - h4 { - display: flex; - align-items: center; - gap: 6px; - font-size: 14px; - font-weight: 600; - color: v.$gray-700; - margin: 0 0 12px; - - .material-icons { - font-size: 18px; - } - } - - .projection-status-list { - display: flex; - flex-direction: column; - gap: 10px; - - .projection-status-item { - padding: 10px; - background: v.$gray-100; - border-radius: 4px; - - .status-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 6px; - - .method-name { - font-size: 13px; - font-weight: 600; - color: v.$gray-800; - } - - .status-badge { - @include w.status-badge; - } - } - - .progress-bar-small { - @include w.progress-bar; - height: 4px; - margin-bottom: 4px; - - .progress-fill-small { - @include w.progress-fill; - } - } - - .status-message { - font-size: 12px; - color: v.$gray-600; - margin: 0; - } - } - } - } - } - - .error-message { - @include w.message-error; - } - - // Action Buttons - .action-buttons { - @include w.action-buttons-footer; - - .btn-secondary { - @include w.btn-secondary; - } - - .buttons-right { - display: flex; - align-items: center; - gap: 12px; - margin-left: auto; - } - - .btn-primary { - @include w.btn-primary; - } - } -} diff --git a/src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.spec.ts b/src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.spec.ts new file mode 100644 index 0000000..1178124 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.spec.ts @@ -0,0 +1,78 @@ +import type { Mocked } from 'vitest'; +import { ProjectionService } from '../../../services/projection.service'; +import { ProjectionConfig } from '../../models/column-config'; +import { DEFAULT_PROJECTION_CONFIG } from '../../models/preprocessing-state'; +import { createSpyObj } from '../../../../testing/create-spy-obj'; +import { buildBackgroundProjectionRuns, enabledBackgroundMethodNames } from './background-projections'; + +const FEATURES = [ + [0.1, 0.2], + [0.3, 0.4], +]; +const IDS = [1, 2]; + +/** Every background method switched on, LLE included (as an older saved config may have it). */ +const EVERYTHING_ON: ProjectionConfig = { + ...DEFAULT_PROJECTION_CONFIG, + enablePCA: true, + enableIsoMap: true, + enableMDS: true, + enableLLE: true, + enableLTSA: true, + enableTSNE: true, + enableUMAP: true, + enableTriMap: true, + enableTopoMap: true, + enableSammon: true, +}; + +describe('background projections', () => { + let service: Mocked; + + beforeEach(() => { + service = createSpyObj('ProjectionService', [ + 'runPCABackground', + 'runIsoMap', + 'runMDS', + 'runLLE', + 'runLTSA', + 'runTSNE', + 'runUMAP', + 'runTriMap', + 'runTopoMap', + 'runSammon', + ]); + }); + + it('never names LLE, which is hidden, even when the config enables it', () => { + expect(enabledBackgroundMethodNames(EVERYTHING_ON)).toEqual([ + 'PCA', + 'IsoMap', + 'MDS', + 'LTSA', + 't-SNE', + 'UMAP', + 'TriMap', + 'TopoMap', + 'Sammon', + ]); + }); + + it('never starts LLE, even when the config enables it', async () => { + const runs = buildBackgroundProjectionRuns(service, EVERYTHING_ON, FEATURES, IDS); + await Promise.all(runs.map(r => r.run())); + + expect(runs.map(r => r.key)).not.toContain('lle'); + expect(service.runLLE).not.toHaveBeenCalled(); + expect(service.runLTSA).toHaveBeenCalledTimes(1); + }); + + it('runs TriMap without parameters (DruidJS defaults)', async () => { + const config: ProjectionConfig = { ...DEFAULT_PROJECTION_CONFIG, enablePCA: false, enableIsoMap: false }; + const runs = buildBackgroundProjectionRuns(service, { ...config, enableTriMap: true }, FEATURES, IDS); + await Promise.all(runs.map(r => r.run())); + + expect(runs.map(r => r.name)).toEqual(['TriMap']); + expect(service.runTriMap).toHaveBeenCalledWith(FEATURES, IDS); + }); +}); diff --git a/src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.ts b/src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.ts new file mode 100644 index 0000000..41129c1 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step6-review-processing/background-projections.ts @@ -0,0 +1,96 @@ +import { ProjectionConfig } from '../../models/column-config'; +import { ProjectionResult, ProjectionService } from '../../../services/projection.service'; +import { ProjectionMethodKey, isProjectionMethodActive } from '../step4-projection-settings/projection-methods'; + +type Ids = (string | number)[]; +type Runner = ( + service: ProjectionService, + features: number[][], + ids: Ids, + config: ProjectionConfig +) => Promise; + +interface BackgroundMethod { + /** ProjectionService method id, also the key of its background status. */ + key: string; + /** Display name. */ + name: string; + /** Projection-config switch; a hidden method never runs (see HIDDEN_PROJECTION_METHODS). */ + configKey: ProjectionMethodKey; + run: Runner; +} + +/** One optional projection to compute in the background after the primary FastMap run. */ +export interface BackgroundProjectionRun { + key: string; + name: string; + run: () => Promise; +} + +/** The optional projection methods, in display order. */ +const BACKGROUND_METHODS: readonly BackgroundMethod[] = [ + { key: 'pca', name: 'PCA', configKey: 'enablePCA', run: (s, f, ids) => s.runPCABackground(f, ids) }, + { + key: 'isomap', + name: 'IsoMap', + configKey: 'enableIsoMap', + run: (s, f, ids, c) => s.runIsoMap(f, ids, { neighbors: c.isomapNeighbors }), + }, + { key: 'mds', name: 'MDS', configKey: 'enableMDS', run: (s, f, ids) => s.runMDS(f, ids) }, + { + key: 'lle', + name: 'LLE', + configKey: 'enableLLE', + run: (s, f, ids, c) => s.runLLE(f, ids, { neighbors: c.lleNeighbors }), + }, + { + key: 'ltsa', + name: 'LTSA', + configKey: 'enableLTSA', + run: (s, f, ids, c) => s.runLTSA(f, ids, { neighbors: c.ltsaNeighbors }), + }, + { + key: 'tsne', + name: 't-SNE', + configKey: 'enableTSNE', + run: (s, f, ids, c) => s.runTSNE(f, ids, { perplexity: c.tsnePerplexity, iterations: c.tsneIterations }), + }, + { + key: 'umap', + name: 'UMAP', + configKey: 'enableUMAP', + run: (s, f, ids, c) => s.runUMAP(f, ids, { neighbors: c.umapNeighbors, minDist: c.umapMinDist }), + }, + { key: 'trimap', name: 'TriMap', configKey: 'enableTriMap', run: (s, f, ids) => s.runTriMap(f, ids) }, + { key: 'topomap', name: 'TopoMap', configKey: 'enableTopoMap', run: (s, f, ids) => s.runTopoMap(f, ids) }, + { key: 'sammon', name: 'Sammon', configKey: 'enableSammon', run: (s, f, ids) => s.runSammon(f, ids) }, +]; + +/** The background methods that run for `config`: switched on and not hidden, in display order. */ +function activeMethods(config: ProjectionConfig): BackgroundMethod[] { + return BACKGROUND_METHODS.filter(m => isProjectionMethodActive(config, m.configKey)); +} + +/** Display names of the background methods that run for `config`, in display order. */ +export function enabledBackgroundMethodNames(config: ProjectionConfig): string[] { + return activeMethods(config).map(m => m.name); +} + +/** Display name of a background status key (e.g. 'tsne' → 't-SNE'). */ +export function backgroundMethodName(key: string): string { + return BACKGROUND_METHODS.find(m => m.key === key)?.name ?? key.toUpperCase(); +} + +/** The runs for every background method that runs for `config`. */ +export function buildBackgroundProjectionRuns( + service: ProjectionService, + config: ProjectionConfig, + features: number[][], + ids: Ids +): BackgroundProjectionRun[] { + return activeMethods(config).map(m => ({ + key: m.key, + name: m.name, + run: () => m.run(service, features, ids, config), + })); +} diff --git a/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.html b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.html new file mode 100644 index 0000000..563e6b6 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.html @@ -0,0 +1,482 @@ +
    +
    + + @if (showProcessing) { +
    + + Your work is saved +
    + } + + + + + @if (!showProcessing) { +
    + +
    +
    +
    +
    +

    Columns Selected

    + {{ enabledColumns }} / {{ totalColumns }} +
    + +
    + +
    +
    +
    +

    + Projection Step {{ projectionStepNumber }} +

    +
    +
    +
    Columns
    +
    + {{ projectionColumns }} included + +
    +
    +
    +
    Methods
    +
    + {{ enabledMethods.join(', ') }} + +
    +
    +
    +
    +
    + +
    +
    +
    +

    + Glyphs & Color Step {{ appearanceStepNumber }} +

    +
    +
    +
    Color
    +
    + {{ colorFeature || 'None' }} · {{ colorScaleName }} + +
    +
    +
    +
    Glyph
    +
    + {{ selectedGlyphFeatures.length }} features + +
    +
    +
    +

    Initial dashboard view — adjustable later in the Encoding panel.

    +
    +
    +
    + + + @if (preflightIssues.length > 0) { +
    + +
    + Heads up — {{ preflightIssues.length }} + {{ preflightIssues.length === 1 ? 'thing' : 'things' }} could cause processing to fail. You can start + anyway, or fix them first. +
    +
    +
    + @for (issue of preflightIssues; track issue.code + issue.title) { + + } +
    + } + + +
    + + + + + + + + + + + + @for (config of columnConfigs; track config.name) { + @if (config.enabled) { + + + + + + + + } + } + +
    ColumnTypeEncodingScalingIn Projection
    + {{ config.name }} + @if (config.isColorFeature) { + + + (color feature) + + } + + + {{ getEncodingLabel(config.encodingMethod) }} + @if (config.targetType === DataType.Numeric || config.targetType === DataType.Date) { + {{ getScalingLabel(config.scalingMethod) }} + } @else { + N/A + } + + @if (config.includeInProjection) { + + + Yes + + } @else { + + + No + + } +
    +
    + + +
    + +
    + Processing Info: Processing time depends on your dataset size and enabled projection + methods. FastMap loads immediately, allowing you to explore your data right away. Additional projections + compute in the background. If anything fails, it appears here with a fix — never a blank screen. +
    +
    +
    + } + + + + + @if (showProcessing) { + + @if (isProcessing) { +
    +
    + +
    +

    {{ processingStep || 'FastMap is computing your primary layout…' }}

    +

    + FastMap loads first so you can explore right away. Other projections keep computing in the background — + if a problem comes up, it shows here, not on a blank page. +

    +
    +
    + +
    +
    +
    + + + +
    + } + + + @if (isErrorScreen && blockingIssue) { +
    + + + + + +
    + +
    +
    + } + + + @if (processingComplete && !isProcessing) { +
    +
    + +
    +

    Your visualization is ready

    +

    + FastMap is loaded — open Glyphspace to start exploring. Additional projections finish in the background. +

    +
    +
    + + @if (isPartialResult) { +
    + +
    + {{ partialIssues.length }} of {{ enabledMethods.length }} projections could not be computed. Your + visualization still works with the ones that succeeded. +
    +
    +
    + @for (issue of partialIssues; track issue.title) { + + } +
    + } @else { +
    +
    + {{ enabledColumns }} columns processed +
    +
    + FastMap projection ready +
    +
    + Data cleaning applied +
    +
    + } + + + + + @if (backgroundItems.length > 0) { +
    +
    +

    + + Background Projections +

    + @if (hasRunningBackground) { + + } +
    +
    + @for (proj of backgroundItems; track proj.method) { +
    +
    + {{ proj.name }} +
    + @switch (proj.status) { + @case ('running') { + Running + } + @case ('complete') { + Complete + } + @case ('error') { + Failed + } + @case ('cancelled') { + Cancelled + } + } + @if (proj.status === 'running') { + + } @else if (proj.canRetry) { + + } +
    +
    + @if (proj.status === 'running') { +
    +
    +
    +

    {{ proj.message }}

    + } + @if (proj.status === 'complete') { +

    Available in Glyphspace dropdown

    + } + @if (proj.status === 'cancelled') { +

    Stopped before it finished. Your other projections are not affected.

    + } +
    + } +
    +
    + } +
    + } + } +
    + + + +
    + @for (m of methodStatuses; track m.name) { +
    +
    + {{ m.name }} + {{ m.role }} +
    + {{ m.statusLabel }} +
    + } +
    +
    + + + @if (!showProcessing) { +
    + +
    + +
    +
    + } + + @if (isErrorScreen) { +
    + +
    + +
    +
    + } + + @if (processingComplete && !isProcessing) { +
    + +
    + + +
    +
    + } +
    diff --git a/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.scss b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.scss new file mode 100644 index 0000000..3c27e43 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.scss @@ -0,0 +1,555 @@ +@use '../../shared/wizard-shared' as w; +@use '../../../../styles/variables' as v; +@use '../../../../styles/mixins' as m; + +.step6-review-processing { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + + // Text for screen readers only (e.g. names of icon-only badges). + .visually-hidden { + @include m.visually-hidden; + } + + .step-content { + @include w.step-content; + } + + // Review Section + .review-section { + // Column tile + one wider card per settings step (Projection, Glyphs & Color). + // Flex (zero basis + min widths, grow 1:2:2) sizes the three like the former + // `minmax(160px, 0.8fr) repeat(2, minmax(240px, 1.6fr))` grid, but wraps + // instead of overflowing when the work area is narrower than the minimums. + // Grow factors stay >= 1 so a card alone on a wrapped line fills it. + .summary-cards { + display: flex; + flex-wrap: wrap; + gap: v.$space-3; + margin-bottom: v.$space-4; + + .summary-card { + flex: 2 1 0; + min-width: min(100%, 240px); + box-sizing: border-box; + display: flex; + gap: v.$space-3; + padding: 14px; + background: white; + border: 1px solid v.$gray-300; + border-radius: 4px; + + &:first-child { + flex-grow: 1; + min-width: min(100%, 160px); + } + + // A6: edit/jump affordance per summary card — uniform, borderless icon button + .card-edit-btn { + margin-left: auto; + align-self: flex-start; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background: transparent; + border: none; + border-radius: 4px; + color: v.$text-muted; + cursor: pointer; + transition: + color 0.15s ease, + background-color 0.15s ease; + + .material-icons { + font-size: v.$icon-size-md; + } + + &:hover { + color: v.$color-accent; + background: rgba(v.$active-color, 0.1); + } + + &:focus-visible { + @include m.focus-ring(1px); + } + } + + .card-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 40px; + height: 40px; + background: rgba(v.$active-color, 0.1); + border-radius: 50%; + + .material-icons { + font-size: v.$icon-size-xl; + color: v.$color-accent; + } + } + + .card-content { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + min-width: 0; + + .card-label { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + font-size: v.$font-size-xs; + font-weight: 600; + color: v.$text-muted; + text-transform: uppercase; + } + + // Which wizard step the card's settings live in. + .card-step { + @include w.severity-badge(info); + padding: 1px 6px; + border-radius: 8px; + font-size: v.$font-size-xs; + letter-spacing: 0.3px; + } + + .card-rows { + display: grid; + gap: 2px; + margin: 6px 0 0; + } + + .card-row { + display: grid; + grid-template-columns: 56px 1fr; + align-items: center; + gap: v.$space-2; + + dt { + font-size: v.$font-size-xs; + font-weight: 600; + color: v.$text-muted; + text-transform: uppercase; + letter-spacing: 0.3px; + } + + dd { + display: flex; + align-items: center; + gap: v.$space-1; + min-width: 0; + margin: 0; + } + + .card-row-value { + flex: 1; + min-width: 0; + font-size: v.$font-size-md; + font-weight: 600; + color: v.$gray-800; + overflow-wrap: anywhere; + } + + .card-edit-btn { + align-self: center; + width: 24px; + height: 24px; + flex-shrink: 0; + } + } + + .card-note { + margin: 6px 0 0; + font-size: v.$font-size-xs; + color: v.$text-muted; + } + + .card-value { + font-size: v.$font-size-lg; + font-weight: 700; + color: v.$gray-800; + } + } + } + } + + .columns-table { + @include w.table-container; + margin-bottom: v.$space-4; + + table { + @include w.table; + } + + .column-name-text { + @include w.column-name-text; + } + + .color-badge { + display: inline-flex; + align-items: center; + margin-left: 6px; + + .material-icons { + font-size: v.$icon-size-xs; + color: v.$color-accent; + } + } + + .text-muted { + font-size: v.$font-size-sm; + color: v.$text-muted; + font-style: italic; + } + + .status-badge { + @include w.status-badge; + } + } + + .info-box { + @include w.info-box; + } + } + + // A3: persistence signifier pill + .persistence-pill { + @include w.severity-badge(info); + display: inline-flex; + align-items: center; + gap: 6px; + align-self: flex-start; + margin-bottom: 14px; + padding: v.$space-1 10px; + border: 1px solid v.$severity-info-border; + border-radius: 12px; + font-size: v.$font-size-sm; + font-weight: 600; + + .material-icons { + font-size: v.$icon-size-sm; + } + } + + // A3: pre-flight banner and partial-results notice (warning tone, neutral text) + .preflight-banner, + .status-panel .partial-notice { + @include w.severity-panel(warning); + display: flex; + gap: 10px; + align-items: flex-start; + padding: v.$space-3 14px; + border-radius: 6px; + font-size: v.$font-size-base; + color: v.$gray-700; + line-height: 1.45; + + .material-icons { + font-size: v.$icon-size-lg; + flex-shrink: 0; + } + } + + .preflight-banner { + margin-bottom: v.$space-3; + } + + .issue-list { + display: flex; + flex-direction: column; + gap: v.$space-3; + margin-bottom: v.$space-4; + } + + // A3: persistent status/error/result panel + .status-panel { + display: flex; + flex-direction: column; + gap: v.$space-4; + padding: 20px; + background: white; + border: 1px solid v.$gray-300; + border-radius: 8px; + + // Shared anatomy of the status hero and the success banner. + .status-hero, + .success-banner { + display: flex; + align-items: center; + gap: v.$space-4; + padding: v.$space-4; + border-radius: 6px; + + .material-icons { + font-size: v.$icon-size-2xl; + flex-shrink: 0; + } + + h3 { + font-size: v.$font-size-lg; + font-weight: 700; + margin: 0 0 v.$space-1; + color: v.$gray-800; + } + + p { + font-size: v.$font-size-base; + color: v.$text-muted; + margin: 0; + line-height: 1.45; + } + } + + .status-hero { + &.processing { + background: v.$severity-info-bg; + + .material-icons { + color: v.$severity-info-icon; + } + } + + &.error { + @include w.severity-panel(error); + } + } + + .success-banner { + @include w.severity-panel(success); + } + + .progress-bar { + @include w.progress-bar; + width: 100%; + height: 8px; + } + + .progress-fill { + @include w.progress-fill; + } + + .progress-text { + font-size: v.$font-size-lg; + font-weight: 700; + color: v.$color-accent; + text-align: center; + } + + .success-stats { + display: flex; + flex-direction: column; + gap: v.$space-2; + + .stat-item { + display: flex; + align-items: center; + gap: v.$space-2; + font-size: v.$font-size-md; + color: v.$gray-700; + + .material-icons { + font-size: v.$icon-size-md; + color: v.$severity-success-icon; + } + } + } + + // Method status bar + .method-bar { + display: flex; + flex-wrap: wrap; + gap: 10px; + + .method-chip { + flex: 1 1 140px; + display: flex; + flex-direction: column; + gap: v.$space-1; + padding: 10px v.$space-3; + border: 1px solid v.$gray-200; + border-radius: 6px; + background: v.$gray-100; + + .chip-top { + display: flex; + align-items: baseline; + gap: 6px; + + .chip-name { + font-size: v.$font-size-base; + font-weight: 700; + color: v.$gray-800; + } + + .chip-role { + font-size: v.$font-size-xs; + text-transform: uppercase; + letter-spacing: 0.03em; + color: v.$text-muted; + } + } + + .chip-status { + font-size: v.$font-size-sm; + font-weight: 600; + color: v.$text-muted; + } + + &.chip-ready { + @include w.severity-panel(success); + + .chip-status { + color: v.$severity-success-text; + } + } + + &.chip-computing { + @include w.severity-panel(info); + + .chip-status { + color: v.$severity-info-text; + } + } + + &.chip-failed { + @include w.severity-panel(error); + + .chip-status { + color: v.$severity-error-text; + } + } + } + } + + .background-status-section { + padding-top: v.$space-2; + border-top: 1px solid v.$gray-200; + + .background-status-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: v.$space-2; + margin-bottom: v.$space-3; + } + + h4 { + display: flex; + align-items: center; + gap: 6px; + font-size: v.$font-size-md; + font-weight: 600; + color: v.$gray-700; + margin: 0; + + .material-icons { + font-size: v.$icon-size-md; + } + + // Receives focus after "Stop all" removes the button that had it. + &:focus-visible { + @include m.focus-ring; + border-radius: 2px; + } + } + + .status-actions { + display: flex; + align-items: center; + gap: v.$space-2; + } + + // Cancel / Retry / Stop all + .btn-background-action { + @include w.btn-text; + font-size: v.$font-size-sm; + padding: 3px v.$space-2; + gap: v.$space-1; + + .material-icons { + font-size: v.$icon-size-xs; + } + } + + .status-badge.status-cancelled { + background: rgba(158, 158, 158, 0.1); + color: v.$text-muted; + } + + .projection-status-list { + display: flex; + flex-direction: column; + gap: 10px; + + .projection-status-item { + padding: 10px; + background: v.$gray-100; + border-radius: 4px; + + .status-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; + + .method-name { + font-size: v.$font-size-base; + font-weight: 600; + color: v.$gray-800; + } + + .status-badge { + @include w.status-badge; + } + } + + .progress-bar-small { + @include w.progress-bar; + height: 4px; + margin-bottom: v.$space-1; + + .progress-fill-small { + @include w.progress-fill; + } + } + + .status-message { + font-size: v.$font-size-sm; + color: v.$text-muted; + margin: 0; + } + } + } + } + } + + // Action Buttons + .action-buttons { + @include w.action-buttons-footer; + + .btn-secondary { + @include w.btn-secondary; + } + + .buttons-right { + display: flex; + align-items: center; + gap: v.$space-3; + margin-left: auto; + } + + .btn-primary { + @include w.btn-primary; + } + } +} diff --git a/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.spec.ts b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.spec.ts new file mode 100644 index 0000000..72838b0 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.spec.ts @@ -0,0 +1,542 @@ +import type { Mock, Mocked } from 'vitest'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BehaviorSubject, Subject, of } from 'rxjs'; +import { Step6ReviewProcessingComponent } from './step6-review-processing.component'; +import { PreprocessingService } from '../../services/preprocessing.service'; +import { DataLoaderService } from '../../../services/data-loader.service'; +import { + BackgroundStatus, + ProjectionCancelledError, + ProjectionResult, + ProjectionService, +} from '../../../services/projection.service'; +import { ToastService } from '../../../services/toast.service'; +import { DEFAULT_PROJECTION_CONFIG, PreprocessingState, ProcessingProgress } from '../../models/preprocessing-state'; +import { ColumnConfig } from '../../models/column-config'; +import { DataProfile } from '../../models/column-statistics'; +import { DatasetCollection } from '../../../shared/interfaces/dataset-collection'; +import { WizardStepId } from '../../shared/constants/step-info'; +import { classifyProcessingError } from '../../shared/constants/wizard-error-classes'; +import { WizardAnnouncer } from '../../shared/wizard-announcer.service'; +import { + DataType, + EncodingMethod, + ScalingMethod, + MissingValueStrategy, + OutlierMethod, + OutlierStrategy, +} from '../../models/data-type.enum'; +import { createSpyObj } from '../../../../testing/create-spy-obj'; +import { createTestPreprocessingService, createToastServiceSpy } from '../../testing/preprocessing-test-helpers'; + +function makeConfig(name: string, isColorFeature = false): ColumnConfig { + return { + name, + originalType: DataType.Numeric, + targetType: DataType.Numeric, + encodingMethod: EncodingMethod.Normalize, + scalingMethod: ScalingMethod.MinMax, + includeInProjection: true, + isColorFeature, + missingValueStrategy: MissingValueStrategy.Keep, + outlierMethod: OutlierMethod.IQR_1_5, + outlierStrategy: OutlierStrategy.Keep, + enabled: true, + hasIssues: false, + }; +} + +function setState(service: PreprocessingService, updates: Partial): void { + (service as unknown as { updateState(u: Partial): void }).updateState(updates); +} + +/** A loaded three-column file on the review step. */ +function seedReview(service: PreprocessingService, updates: Partial = {}): void { + const names = ['x', 'y', 'z']; + const profile: DataProfile = { + totalRows: 100, + totalColumns: names.length, + fileSize: 1000, + fileName: 'test.csv', + columns: names.map(name => ({ + name, + dataType: DataType.Numeric, + count: 100, + missingCount: 0, + missingPercentage: 0, + uniqueCount: 50, + })), + qualityScore: 90, + duplicateCount: 0, + previewRows: [], + }; + setState(service, { + rawFileName: 'test.csv', + dataProfile: profile, + columnConfigs: new Map(names.map(name => [name, makeConfig(name, name === 'x')])), + glyphFeatures: [...names], + ...updates, + }); + service.goToStep('review'); +} + +/** Only FastMap: no background projection is started by a processing run. */ +const NO_BACKGROUND_METHODS = { ...DEFAULT_PROJECTION_CONFIG, enablePCA: false, enableIsoMap: false }; + +/** + * Review & Process summary: Projection and Glyphs & Color are separate cards, + * and every edit button deep-links to the step (by id) and anchor that owns the + * setting. The processing pipeline itself is not exercised here. + */ +describe('Step6ReviewProcessingComponent – summary and deep links', () => { + let service: PreprocessingService; + let toast: Mocked; + + function seed(): void { + seedReview(service); + } + + beforeEach(async () => { + localStorage.clear(); + service = createTestPreprocessingService(); + seed(); + toast = createToastServiceSpy(); + await TestBed.configureTestingModule({ + imports: [Step6ReviewProcessingComponent], + providers: [ + { provide: PreprocessingService, useValue: service }, + { provide: DataLoaderService, useValue: {} }, + { provide: ProjectionService, useValue: { backgroundStatusObservable: of(new Map()) } }, + { provide: ToastService, useValue: toast }, + ], + }).compileComponents(); + }); + + afterEach(() => localStorage.clear()); + + function render(): HTMLElement { + const fixture = TestBed.createComponent(Step6ReviewProcessingComponent); + fixture.detectChanges(); + return fixture.nativeElement as HTMLElement; + } + + function cardTitles(el: HTMLElement): string[] { + return Array.from(el.querySelectorAll('.summary-card .card-label')).map(h => + (h.textContent ?? '').replace(/\s+/g, ' ').trim() + ); + } + + it('shows Projection and Glyphs & Color as separate cards with their step numbers', () => { + expect(cardTitles(render())).toEqual(['Columns Selected', 'Projection Step 4', 'Glyphs & Color Step 5']); + }); + + const editButtons: { label: string; step: WizardStepId; anchor: string }[] = [ + { label: 'Edit column selection', step: 'columns', anchor: 'wizard-anchor-columns' }, + { label: 'Edit projection columns', step: 'projection', anchor: 'wizard-anchor-projection-columns' }, + { label: 'Edit projection methods', step: 'projection', anchor: 'wizard-anchor-methods' }, + { label: 'Edit color attribute and scale', step: 'appearance', anchor: 'wizard-anchor-color' }, + { label: 'Edit glyph features', step: 'appearance', anchor: 'wizard-anchor-glyph' }, + ]; + + editButtons.forEach(({ label, step, anchor }) => { + it(`"${label}" opens the ${step} step at ${anchor}`, () => { + const button = render().querySelector(`button[aria-label="${label}"]`); + expect(button, label).not.toBeNull(); + + button?.click(); + + expect(service.currentStepId).toBe(step); + expect(service.consumeScrollTarget()).toBe(anchor); + }); + }); + + it('shows the color attribute, scale and glyph count on the Glyphs & Color card', () => { + const text = (render().textContent ?? '').replace(/\s+/g, ' '); + expect(text).toContain('x · Diverge'); + expect(text).toContain('3 features'); + }); + + it('does not list LLE, which is hidden, even when a saved config enables it', () => { + seedReview(service, { projectionConfig: { ...NO_BACKGROUND_METHODS, enableLLE: true, enableLTSA: true } }); + + const methodsRow = render().querySelector('button[aria-label="Edit projection methods"]')?.closest('dd'); + const methods = methodsRow?.querySelector('.card-row-value')?.textContent?.trim(); + + expect(methods).toBe('FastMap (Primary), LTSA'); + }); + + it('"Fix in step N" jumps to the issue step by id and names it in the toast', () => { + const fixture = TestBed.createComponent(Step6ReviewProcessingComponent); + const issue = classifyProcessingError('A glyph needs 3-12 glyph features'); + + fixture.componentInstance.fixInStep(issue); + + expect(service.currentStepId).toBe('appearance'); + expect(service.consumeScrollTarget()).toBe('wizard-anchor-glyph'); + expect(toast.info).toHaveBeenCalledWith('Opening Step 5 · Glyphs & Color', 2500); + }); +}); + +/** + * A full primary run: Python processing, then FastMap. The service's + * isProcessing flag (which the shell uses to block undo/redo) must stay up until + * FastMap has finished, and screen readers hear each phase once. + */ +describe('Step6ReviewProcessingComponent – processing run', () => { + let service: PreprocessingService; + let announcer: WizardAnnouncer; + let progress$: Subject; + let runFastMapSync: Mock; + let terminateAllWorkers: Mock; + let clearBackgroundStatuses: Mock; + let detachAll: Mock; + let runLLE: Mock; + + function dataProcessor(): object { + return { + processingProgress: progress$.asObservable(), + processWithConfig: () => { + progress$.next({ step: 'Building dataset', progress: 86, message: 'Row 1000' }); + progress$.next({ step: 'Building dataset', progress: 90, message: 'Row 2000' }); + progress$.next({ step: 'Complete', progress: 100, message: 'Processing successful' }); + return Promise.resolve({ datasets: { d: {} } }); + }, + getProcessedFeatures: () => Promise.resolve('id,f\n1,0.5\n2,0.7\n'), + pruneFiles: () => Promise.resolve(), + }; + } + + beforeEach(async () => { + localStorage.clear(); + progress$ = new Subject(); + service = createTestPreprocessingService(dataProcessor()); + seedReview(service, { projectionConfig: NO_BACKGROUND_METHODS }); + runFastMapSync = vi + .fn() + .mockResolvedValue({ method: 'fastmap', positions: [{ id: 1, x: 0, y: 0 }], computeTime: 3 }); + terminateAllWorkers = vi.fn().mockReturnValue([]); + clearBackgroundStatuses = vi.fn(); + detachAll = vi.fn(); + runLLE = vi.fn(); + const projection = { + backgroundStatusObservable: of(new Map()), + parseCSVFeatures: () => ({ features: [[0.5], [0.7]], ids: [1, 2] }), + runFastMapSync, + terminateAllWorkers, + clearBackgroundStatuses, + detachAll, + runLLE, + }; + await TestBed.configureTestingModule({ + imports: [Step6ReviewProcessingComponent], + providers: [ + { provide: PreprocessingService, useValue: service }, + { provide: DataLoaderService, useValue: {} }, + { provide: ProjectionService, useValue: projection }, + { provide: ToastService, useValue: createToastServiceSpy() }, + ], + }).compileComponents(); + announcer = TestBed.inject(WizardAnnouncer); + vi.spyOn(announcer, 'announce').mockImplementation(() => undefined); + }); + + afterEach(() => localStorage.clear()); + + function create(): ComponentFixture { + const fixture = TestBed.createComponent(Step6ReviewProcessingComponent); + fixture.detectChanges(); + // Skip the minimum loading-screen time; it only exists so sighted users notice a fast run. + (fixture.componentInstance as unknown as { MIN_LOADING_MS: number }).MIN_LOADING_MS = 0; + return fixture; + } + + function failFastMap(): void { + runFastMapSync.mockRejectedValue(new Error('FastMap failed: boom')); + vi.spyOn(console, 'error').mockImplementation(() => undefined); // the component logs the failure + } + + it('keeps the service processing, so undo is blocked, until FastMap has finished', async () => { + service.pushHistory('Earlier change'); + let processingDuringFastMap = false; + let undoDuringFastMap: unknown = 'not called'; + runFastMapSync.mockImplementation(async () => { + processingDuringFastMap = service.currentState.isProcessing; + undoDuringFastMap = service.undo(); + return { method: 'fastmap', positions: [], computeTime: 1 }; + }); + + await create().componentInstance.startProcessing(); + + expect(processingDuringFastMap).toBe(true); + expect(undoDuringFastMap).toBeNull(); + expect(service.currentState.isProcessing).toBe(false); + expect(service.canUndo).toBe(true); + }); + + it('lets background projections of an earlier dataset finish, detached, on a fresh run', async () => { + await create().componentInstance.startProcessing(); + + expect(detachAll).toHaveBeenCalled(); + expect(terminateAllWorkers).not.toHaveBeenCalled(); + expect(clearBackgroundStatuses).not.toHaveBeenCalled(); + }); + + it('stops and forgets the outdated background projections when reprocessing', async () => { + setState(service, { processedDataset: { datasets: { d: {} } } as unknown as DatasetCollection }); + + await create().componentInstance.startProcessing(); + + expect(terminateAllWorkers).toHaveBeenCalled(); + expect(clearBackgroundStatuses).toHaveBeenCalledBefore(runFastMapSync); + expect(detachAll).not.toHaveBeenCalled(); + }); + + it('does not start LLE, which is hidden, even when a saved config enables it', async () => { + seedReview(service, { projectionConfig: { ...NO_BACKGROUND_METHODS, enableLLE: true } }); + + await create().componentInstance.startProcessing(); + + expect(runFastMapSync).toHaveBeenCalled(); + expect(runLLE).not.toHaveBeenCalled(); + }); + + it('releases the processing flag when FastMap fails', async () => { + failFastMap(); + const fixture = create(); + + await fixture.componentInstance.startProcessing(); + + expect(service.currentState.isProcessing).toBe(false); + expect(fixture.componentInstance.isErrorScreen).toBe(true); + }); + + it('announces the start, each phase once (not every tick) and the completion', async () => { + await create().componentInstance.startProcessing(); + + const messages = vi.mocked(announcer.announce).mock.calls.map(args => args[0]); + expect(messages).toEqual([ + 'Processing started.', + 'Building dataset.', + 'Loading features for projections.', + 'Computing FastMap projection.', + 'Loading dataset with FastMap.', + 'Processing complete. Your visualization is ready to open in Glyphspace.', + ]); + }); + + it('reports a failure once, through the blocking error alert with its cause', async () => { + failFastMap(); + const fixture = create(); + + await fixture.componentInstance.startProcessing(); + fixture.detectChanges(); + + const alert = (fixture.nativeElement as HTMLElement).querySelector('.status-hero.error[role="alert"]'); + const cause = fixture.componentInstance.blockingIssue?.title ?? 'missing'; + expect(alert?.textContent).toContain(`Cause: ${cause}.`); + // role="alert" is already an assertive announcement; the announcer must not repeat it. + const messages = vi.mocked(announcer.announce).mock.calls.map(args => args[0]); + expect(messages.some(m => /fail/i.test(m))).toBe(false); + }); + + it('exposes the processing progress as a labelled progressbar', () => { + const fixture = create(); + Object.assign(fixture.componentInstance, { + showProcessing: true, + isProcessing: true, + processingProgress: 45, + processingStep: 'Encoding features', + }); + fixture.detectChanges(); + + const bar = (fixture.nativeElement as HTMLElement).querySelector('[role="progressbar"]'); + expect(bar?.getAttribute('aria-label')).toBe('Processing progress'); + expect(bar?.getAttribute('aria-valuemin')).toBe('0'); + expect(bar?.getAttribute('aria-valuemax')).toBe('100'); + expect(bar?.getAttribute('aria-valuenow')).toBe('45'); + expect(bar?.getAttribute('aria-valuetext')).toBe('45% – Encoding features'); + }); +}); + +/** + * Background projections can be cancelled one by one or all at once, and a + * cancelled or failed one can be retried on its own, without resetting the wizard. + */ +describe('Step6ReviewProcessingComponent – cancel and retry background projections', () => { + let service: PreprocessingService; + let statuses: BehaviorSubject>; + let projection: Mocked; + let announcer: WizardAnnouncer; + let toast: Mocked; + let dataLoader: Mocked; + let retryable: Set; + + type Entry = [string, BackgroundStatus]; + const entry = (method: string, status: BackgroundStatus['status'], progress = 0): Entry => [ + method, + { method, status, progress, message: `${method} ${status}` }, + ]; + const initial: Entry[] = [ + entry('pca', 'complete', 100), + entry('umap', 'running', 40), + entry('tsne', 'error'), + entry('mds', 'cancelled'), + ]; + + /** Publish the initial statuses with the given entries replaced. */ + function publish(...changes: Entry[]): void { + statuses.next(new Map([...initial, ...changes])); + } + + beforeEach(async () => { + localStorage.clear(); + service = createTestPreprocessingService({ pruneFiles: () => Promise.resolve() }); + seedReview(service, { + projectionConfig: { + ...NO_BACKGROUND_METHODS, + enablePCA: true, + enableMDS: true, + enableTSNE: true, + enableUMAP: true, + }, + processedDataset: { datasets: { d: {} } } as unknown as DatasetCollection, + }); + statuses = new BehaviorSubject(new Map()); + retryable = new Set(['tsne', 'mds']); + projection = createSpyObj( + 'ProjectionService', + ['cancel', 'terminateAllWorkers', 'canRetry', 'retry', 'detachAll'], + { backgroundStatusObservable: statuses.asObservable() } + ); + projection.canRetry.mockImplementation(method => retryable.has(method)); + toast = createToastServiceSpy(); + dataLoader = createSpyObj('DataLoaderService', [ + 'addProcessedDatasetToCollection', + 'loadProcessedDataset', + 'saveDatasetToStorage', + ]); + await TestBed.configureTestingModule({ + imports: [Step6ReviewProcessingComponent], + providers: [ + { provide: PreprocessingService, useValue: service }, + { provide: DataLoaderService, useValue: dataLoader }, + { provide: ProjectionService, useValue: projection }, + { provide: ToastService, useValue: toast }, + ], + }).compileComponents(); + announcer = TestBed.inject(WizardAnnouncer); + vi.spyOn(announcer, 'announce').mockImplementation(() => undefined); + }); + + afterEach(() => localStorage.clear()); + + function render(): { fixture: ComponentFixture; el: HTMLElement } { + const fixture = TestBed.createComponent(Step6ReviewProcessingComponent); + fixture.detectChanges(); + publish(); + fixture.detectChanges(); + return { fixture, el: fixture.nativeElement as HTMLElement }; + } + + function button(el: HTMLElement, label: string): HTMLButtonElement | null { + return el.querySelector(`button[aria-label="${label}"]`); + } + + function buttonByText(el: HTMLElement, text: string): HTMLButtonElement | undefined { + return Array.from(el.querySelectorAll('button')).find(b => b.textContent?.includes(text)); + } + + const flush = (): Promise => new Promise(resolve => setTimeout(resolve)); + + it('offers Cancel while running and Retry once cancelled or failed', () => { + const { el } = render(); + + expect(button(el, 'Cancel UMAP')).not.toBeNull(); + expect(button(el, 'Retry t-SNE')).not.toBeNull(); + expect(button(el, 'Retry MDS')).not.toBeNull(); + expect(button(el, 'Cancel PCA')).toBeNull(); + expect(button(el, 'Retry PCA')).toBeNull(); + expect(buttonByText(el, 'Stop all background projections')).toBeDefined(); + }); + + it('shows the cancelled state in the list and in the method bar', () => { + const { el } = render(); + + expect(el.querySelector('.status-badge.status-cancelled')?.textContent).toContain('Cancelled'); + const mdsChip = Array.from(el.querySelectorAll('.method-chip')).find(c => c.textContent?.includes('MDS')); + expect(mdsChip?.querySelector('.chip-status')?.textContent?.trim()).toBe('Cancelled'); + }); + + it('labels the progressbar of each running projection', () => { + const bar = render().el.querySelector('.projection-status-item [role="progressbar"]'); + expect(bar?.getAttribute('aria-label')).toBe('UMAP progress'); + expect(bar?.getAttribute('aria-valuenow')).toBe('40'); + }); + + it('cancels just that method, announces it and moves focus to its Retry button', () => { + const { el } = render(); + projection.cancel.mockImplementation(method => { + retryable.add(method); + publish(entry(method, 'cancelled')); + return true; + }); + + button(el, 'Cancel UMAP')?.click(); + + expect(projection.cancel).toHaveBeenCalledExactlyOnceWith('umap'); + expect(announcer.announce).toHaveBeenCalledWith('UMAP cancelled. You can retry it.'); + expect(document.activeElement?.getAttribute('aria-label')).toBe('Retry UMAP'); + }); + + it('stops all running projections and announces how many were stopped', () => { + const { el } = render(); + projection.terminateAllWorkers.mockImplementation(() => { + publish(entry('umap', 'cancelled')); + return ['umap']; + }); + + buttonByText(el, 'Stop all background projections')?.click(); + + expect(projection.terminateAllWorkers).toHaveBeenCalled(); + expect(announcer.announce).toHaveBeenCalledWith('Stopped 1 background projection.'); + expect(buttonByText(el, 'Stop all background projections')).toBeUndefined(); + expect(document.activeElement?.id).toBe('step6-background-heading'); + }); + + it('retries only that method and adds its result to the dataset', async () => { + const { el } = render(); + const result: ProjectionResult = { method: 'tsne', positions: [{ id: 1, x: 1, y: 2 }], computeTime: 1500 }; + projection.retry.mockImplementation(method => { + retryable.delete(method); + publish(entry(method, 'running')); + return Promise.resolve(result); + }); + + button(el, 'Retry t-SNE')?.click(); + await flush(); + + expect(projection.retry).toHaveBeenCalledExactlyOnceWith('tsne'); + expect(announcer.announce).toHaveBeenCalledWith('Retrying t-SNE.'); + expect(document.activeElement?.getAttribute('aria-label')).toBe('Cancel t-SNE'); + expect(dataLoader.loadProcessedDataset).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith('t-SNE projection ready! (1.5s)', 4000); + }); + + it('shows no error when a retried run is cancelled again', async () => { + const { el } = render(); + projection.retry.mockReturnValue(Promise.reject(new ProjectionCancelledError('tsne'))); + + button(el, 'Retry t-SNE')?.click(); + await flush(); + + expect(toast.error).not.toHaveBeenCalled(); + }); + + it('detaches the running projections when the dataset is handed to the dashboard', () => { + const { fixture } = render(); + + fixture.componentInstance.goToDashboard(); + + expect(projection.detachAll).toHaveBeenCalled(); + }); +}); diff --git a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.ts similarity index 54% rename from src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts rename to src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.ts index 58f8849..7929ce9 100644 --- a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts +++ b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.ts @@ -3,37 +3,93 @@ import { NgTemplateOutlet } from '@angular/common'; import { Subscription } from 'rxjs'; import { PreprocessingService } from '../../services/preprocessing.service'; import { DataLoaderService } from '../../../services/data-loader.service'; -import { ProjectionService, ProjectionResult } from '../../../services/projection.service'; +import { + BackgroundStatus, + ProjectionCancelledError, + ProjectionResult, + ProjectionService, +} from '../../../services/projection.service'; import { ToastService } from '../../../services/toast.service'; import { ColumnConfig, ProjectionConfig } from '../../models/column-config'; +import { DEFAULT_PROJECTION_CONFIG } from '../../models/preprocessing-state'; import { DataType, getEncodingLabel as encLabelFn, getScalingLabel as scaleLabelFn } from '../../models/data-type.enum'; -import { STEP_INFO } from '../../shared/constants/step-info'; +import { WizardStepId, getStepInfo, stepNumber } from '../../shared/constants/step-info'; import { DataTypeBadgeComponent } from '../../shared/data-type-badge/data-type-badge.component'; import { WizardIssueCardComponent } from '../../shared/wizard-issue-card/wizard-issue-card.component'; +import { WizardAnnouncer } from '../../shared/wizard-announcer.service'; +import { getColorScaleById } from '../../../shared/interfaces/color-scale'; import { WizardIssue, - STEP_NAMES, classifyProcessingError, classifyBackgroundFailure, detectPreflightIssues, } from '../../shared/constants/wizard-error-classes'; +import { + backgroundMethodName, + buildBackgroundProjectionRuns, + enabledBackgroundMethodNames, +} from './background-projections'; + +type ChipStatus = 'ready' | 'computing' | 'queued' | 'failed' | 'cancelled'; /** One entry in the method status bar (FastMap / PCA / UMAP / …). */ interface MethodStatus { name: string; role: string; - status: 'ready' | 'computing' | 'queued' | 'failed'; + status: ChipStatus; statusLabel: string; } +/** One row of the background-projection status list. */ +export interface BackgroundItem { + method: string; // status key, e.g. 'tsne' + name: string; // display name, e.g. 't-SNE' + status: BackgroundStatus['status']; + progress: number; + message: string; + canRetry: boolean; +} + +/** Feature matrix handed from the primary run to the background projections. */ +interface PrimaryRunFeatures { + features: number[][]; + ids: (string | number)[]; +} + +const CHIP_LABELS: Record = { + ready: 'Ready', + computing: 'Computing…', + queued: 'Queued', + failed: 'Failed', + cancelled: 'Cancelled', +}; + +const BACKGROUND_CHIP_STATUS: Record = { + pending: 'queued', + running: 'computing', + complete: 'ready', + error: 'failed', + cancelled: 'cancelled', +}; + +/** Heading of the background list; takes focus after "Stop all" removes the button. */ +const BACKGROUND_HEADING_ID = 'step6-background-heading'; + +function methodRole(label: string): string { + if (label.startsWith('FastMap')) return 'Primary'; + if (label === 'PCA') return 'Instant'; + if (label === 't-SNE' || label === 'Sammon') return 'Slow'; + return 'Background'; +} + @Component({ - selector: 'app-step5-review-processing', + selector: 'app-step6-review-processing', standalone: true, imports: [NgTemplateOutlet, DataTypeBadgeComponent, WizardIssueCardComponent], - templateUrl: './step5-review-processing.component.html', - styleUrl: './step5-review-processing.component.scss', + templateUrl: './step6-review-processing.component.html', + styleUrl: './step6-review-processing.component.scss', }) -export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { +export class Step6ReviewProcessingComponent implements OnInit, OnDestroy { @Output() wizardFinish = new EventEmitter(); getEncodingLabel = encLabelFn; getScalingLabel = scaleLabelFn; @@ -45,8 +101,9 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { enabledMethods: string[] = []; columnConfigs: ColumnConfig[] = []; colorFeature: string | null = null; + colorScaleName = ''; selectedGlyphFeatures: string[] = []; - projectionConfig!: ProjectionConfig; + projectionConfig: ProjectionConfig = { ...DEFAULT_PROJECTION_CONFIG }; // Processing state isProcessing = false; @@ -56,17 +113,16 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { error: string | null = null; showProcessing = false; - // Background projection status - backgroundProjections = new Map(); + // Background projection status, by method key, and the list rows derived from it + backgroundProjections = new Map(); + backgroundItems: BackgroundItem[] = []; + readonly backgroundHeadingId = BACKGROUND_HEADING_ID; // A3: structured, persistent issues. preflightIssues: WizardIssue[] = []; // shown on the review screen before processing blockingIssue: WizardIssue | null = null; // primary (hard) failure — replaces the old bare "error" string partialIssues: WizardIssue[] = []; // degraded: some background projections failed (dismissable) private dismissedMethods = new Set(); - private expandedTechnical = new Set(); - - readonly STEP_NAMES = STEP_NAMES; // Capture dataset info for background projections (survives wizard reset) private backgroundDatasetName = ''; @@ -75,26 +131,38 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { // Expose enums DataType = DataType; - readonly stepInfo = STEP_INFO[4]; // Step 5 (index 4) + readonly stepInfo = getStepInfo('review'); + // Step numbers shown on the summary cards, e.g. "Step 4". + readonly projectionStepNumber = stepNumber('projection'); + readonly appearanceStepNumber = stepNumber('appearance'); private progressSubscription?: Subscription; private backgroundStatusSubscription?: Subscription; + // True while a startProcessing() run is in flight (re-entry guard). + private runActive = false; + // Set in ngOnDestroy: async continuations must not touch the destroyed view. + private destroyed = false; + // Last processing phase read out, so progress ticks within a phase stay silent. + private lastAnnouncedPhase = ''; + constructor( public preprocessingService: PreprocessingService, private dataLoader: DataLoaderService, private projectionService: ProjectionService, private toastService: ToastService, private cdr: ChangeDetectorRef, - private ngZone: NgZone + private ngZone: NgZone, + private announcer: WizardAnnouncer ) {} ngOnInit(): void { const state = this.preprocessingService.currentState; - // Load color feature + // Load color feature and scale const colorCol = Array.from(state.columnConfigs.values()).find(c => c.isColorFeature); this.colorFeature = colorCol?.name ?? null; + this.colorScaleName = getColorScaleById(state.colorScaleId).name; // Load glyph features this.selectedGlyphFeatures = [...state.glyphFeatures]; @@ -136,6 +204,7 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { + this.destroyed = true; if (this.progressSubscription) { this.progressSubscription.unsubscribe(); } @@ -157,17 +226,7 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { this.projectionColumns = this.columnConfigs.filter(c => c.enabled && c.includeInProjection).length; // FastMap is always the primary projection - this.enabledMethods = ['FastMap (Primary)']; - if (this.projectionConfig.enablePCA) this.enabledMethods.push('PCA'); - if (this.projectionConfig.enableIsoMap) this.enabledMethods.push('IsoMap'); - if (this.projectionConfig.enableMDS) this.enabledMethods.push('MDS'); - if (this.projectionConfig.enableLLE) this.enabledMethods.push('LLE'); - if (this.projectionConfig.enableLTSA) this.enabledMethods.push('LTSA'); - if (this.projectionConfig.enableTSNE) this.enabledMethods.push('t-SNE'); - if (this.projectionConfig.enableUMAP) this.enabledMethods.push('UMAP'); - if (this.projectionConfig.enableTriMap) this.enabledMethods.push('TriMap'); - if (this.projectionConfig.enableTopoMap) this.enabledMethods.push('TopoMap'); - if (this.projectionConfig.enableSammon) this.enabledMethods.push('Sammon'); + this.enabledMethods = ['FastMap (Primary)', ...enabledBackgroundMethodNames(this.projectionConfig)]; } // ============================================================================ @@ -184,7 +243,7 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { this.blockingIssue = null; this.partialIssues = []; this.dismissedMethods.clear(); - this.expandedTechnical.clear(); + this.lastAnnouncedPhase = ''; // Clear any stale error left in the singleton state (P-S5-03). this.preprocessingService.clearError(); } @@ -193,10 +252,28 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { this.ngZone.run(() => { this.processingStep = step; this.processingProgress = progress; - this.cdr.detectChanges(); + this.announcePhase(step); + this.detectChangesIfAlive(); }); } + /** Read out a new processing phase once; repeated ticks of the same phase stay silent. */ + private announcePhase(phase: string): void { + const text = phase.replace(/(\.\.\.|…)$/, '').trim(); + if (!text || text === this.lastAnnouncedPhase) { + return; + } + this.lastAnnouncedPhase = text; + this.announcer.announce(`${text}.`); + } + + /** Render now, unless the step was destroyed while an async run continued. */ + private detectChangesIfAlive(): void { + if (!this.destroyed) { + this.cdr.detectChanges(); + } + } + /** Minimum time the loading hero stays on screen so a fast run -- or a fast * failure -- is actually perceivable instead of a one-frame flash. */ private readonly MIN_LOADING_MS = 600; @@ -216,18 +293,34 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { } async startProcessing(): Promise { + // Re-entry guard: a parallel run would race this one and its `finally` + // would tear down this run's progress subscription. + if (this.runActive) { + return; + } + this.runActive = true; + + this.releaseEarlierBackgroundRuns(); this.resetProcessingState(); this.showProcessing = true; this.isProcessing = true; this.processingStep = 'Initializing...'; + this.announcer.announce('Processing started.'); - this.progressSubscription = this.preprocessingService.processingProgress.subscribe({ + this.progressSubscription?.unsubscribe(); + const progressSubscription = this.preprocessingService.processingProgress.subscribe({ next: progress => { this.processingStep = progress.message || progress.step; this.processingProgress = Math.min(progress.progress, 70); - this.cdr.detectChanges(); + // Python's final 100 % step is not read out: FastMap still has to run, + // and completion is announced once the dataset is loaded. + if (progress.progress < 100) { + this.announcePhase(progress.step); + } + this.detectChangesIfAlive(); }, }); + this.progressSubscription = progressSubscription; // Keep the loading hero visible for a perceptible minimum, measured from // here, and let the browser paint it before the heavy work begins. @@ -235,55 +328,95 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { await this.paintYield(); try { - await this.preprocessingService.processData(); + // isProcessing stays up in the service for the whole primary run (not just + // processData), so the shell keeps undo/redo disabled during FastMap too. + const primary = await this.preprocessingService.whileProcessing(() => this.runPrimaryProcessing(startedAt)); + this.startBackgroundProjections(primary.features, primary.ids); + } catch (error: unknown) { + console.error('Processing failed:', error); + await this.ensureMinVisible(startedAt); + this.showProcessingError(error); + } finally { + // Only release what this run created. + progressSubscription.unsubscribe(); + if (this.progressSubscription === progressSubscription) { + this.progressSubscription = undefined; + } + this.runActive = false; + } + } - this.updateProcessingUI('Loading features for projections...', 70); + /** + * Before a new run: when reprocessing this wizard's dataset, its earlier + * background projections are outdated, so stop them and clear their statuses + * (Retry must not re-run old data). Otherwise they belong to a dataset that + * was already loaded into the dashboard: let them finish, but detached. + */ + private releaseEarlierBackgroundRuns(): void { + if (this.preprocessingService.currentState.processedDataset) { + this.projectionService.terminateAllWorkers(); + this.projectionService.clearBackgroundStatuses(); + } else { + this.projectionService.detachAll(); + } + } - const csvText = await this.preprocessingService.getProcessedFeaturesCSV(); - const { features, ids } = this.projectionService.parseCSVFeatures(csvText); + /** + * Python processing, then the primary FastMap projection. + * + * Deliberately not cancellable: the Python step runs in the shared Pyodide + * worker, and the only way to interrupt it is to terminate that worker, which + * drops the loaded Python runtime (a slow reload) and the uploaded file held + * in its virtual FS, so the user would have to upload again. FastMap runs + * synchronously on the main thread and cannot be interrupted either. Only the + * optional background projections (one worker each) offer Cancel. + */ + private async runPrimaryProcessing(startedAt: number): Promise { + await this.preprocessingService.processData(); - this.updateProcessingUI('Computing FastMap projection...', 75); + this.updateProcessingUI('Loading features for projections...', 70); - // Use FastMap as the primary projection - const fastmapResult = await this.projectionService.runFastMapSync(features, ids); + const csvText = await this.preprocessingService.getProcessedFeaturesCSV(); + const { features, ids } = this.projectionService.parseCSVFeatures(csvText); - this.updateProcessingUI('Loading dataset with FastMap...', 90); + this.updateProcessingUI('Computing FastMap projection...', 75); - await this.preprocessingService.addProjectionPositions('fastmap', fastmapResult.positions); + // Use FastMap as the primary projection + const fastmapResult = await this.projectionService.runFastMapSync(features, ids); - await this.ensureMinVisible(startedAt); - this.ngZone.run(() => { - this.processingProgress = 100; - this.processingStep = `Dataset loaded with FastMap (${fastmapResult.computeTime}ms)`; - this.processingComplete = true; - this.isProcessing = false; - this.cdr.detectChanges(); - }); + this.updateProcessingUI('Loading dataset with FastMap...', 90); - this.startBackgroundProjections(features, ids); - } catch (error: unknown) { - console.error('Processing failed:', error); - await this.ensureMinVisible(startedAt); - this.ngZone.run(() => { - const raw = error instanceof Error ? error.message : String(error); - this.error = raw; - // A3: classify into a persistent, actionable issue (cause + fix + target step). - this.blockingIssue = classifyProcessingError(raw); - this.isProcessing = false; - this.processingComplete = false; - this.cdr.detectChanges(); - }); - } finally { - if (this.progressSubscription) { - this.progressSubscription.unsubscribe(); - this.progressSubscription = undefined; - } - } + await this.preprocessingService.addProjectionPositions('fastmap', fastmapResult.positions); + + await this.ensureMinVisible(startedAt); + this.ngZone.run(() => { + this.processingProgress = 100; + this.processingStep = `Dataset loaded with FastMap (${fastmapResult.computeTime}ms)`; + this.processingComplete = true; + this.isProcessing = false; + this.announcer.announce('Processing complete. Your visualization is ready to open in Glyphspace.'); + this.detectChangesIfAlive(); + }); + + return { features, ids }; } - private async startBackgroundProjections(features: number[][], ids: (string | number)[]): Promise { - const config = this.projectionConfig; + private showProcessingError(error: unknown): void { + this.ngZone.run(() => { + const raw = error instanceof Error ? error.message : String(error); + this.error = raw; + // A3: classify into a persistent, actionable issue (cause + fix + target step). + // Screen readers hear it through the error panel's role="alert" (an + // assertive announcement including the cause); announcing it through the + // WizardAnnouncer as well would read the failure twice. + this.blockingIssue = classifyProcessingError(raw); + this.isProcessing = false; + this.processingComplete = false; + this.detectChangesIfAlive(); + }); + } + private startBackgroundProjections(features: number[][], ids: (string | number)[]): void { // Capture dataset info so background projections can add positions even after wizard reset const state = this.preprocessingService.currentState; this.backgroundDatasetName = state.datasetName; @@ -291,57 +424,9 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { // Note: the background-status subscription is created once in ngOnInit so it // also restores the method bar after the wizard is reopened. - - // Data-driven projection registry: each entry maps a config flag to its runner - const projections: { enabled: boolean; name: string; run: () => Promise }[] = [ - { enabled: config.enablePCA, name: 'PCA', run: () => this.projectionService.runPCABackground(features, ids) }, - { - enabled: config.enableIsoMap, - name: 'IsoMap', - run: () => this.projectionService.runIsoMap(features, ids, { neighbors: config.isomapNeighbors }), - }, - { enabled: config.enableMDS, name: 'MDS', run: () => this.projectionService.runMDS(features, ids) }, - { - enabled: config.enableLLE, - name: 'LLE', - run: () => this.projectionService.runLLE(features, ids, { neighbors: config.lleNeighbors }), - }, - { - enabled: config.enableLTSA, - name: 'LTSA', - run: () => this.projectionService.runLTSA(features, ids, { neighbors: config.ltsaNeighbors }), - }, - { - enabled: config.enableTSNE, - name: 't-SNE', - run: () => - this.projectionService.runTSNE(features, ids, { - perplexity: config.tsnePerplexity, - iterations: config.tsneIterations, - }), - }, - { - enabled: config.enableUMAP, - name: 'UMAP', - run: () => - this.projectionService.runUMAP(features, ids, { - neighbors: config.umapNeighbors, - minDist: config.umapMinDist, - }), - }, - { - enabled: config.enableTriMap, - name: 'TriMap', - run: () => this.projectionService.runTriMap(features, ids, { weightAdj: config.trimapWeightAdj }), - }, - { enabled: config.enableTopoMap, name: 'TopoMap', run: () => this.projectionService.runTopoMap(features, ids) }, - { enabled: config.enableSammon, name: 'Sammon', run: () => this.projectionService.runSammon(features, ids) }, - ]; - - for (const proj of projections) { - if (proj.enabled) { - this.runBackgroundProjection(proj.name, proj.run); - } + const runs = buildBackgroundProjectionRuns(this.projectionService, this.projectionConfig, features, ids); + for (const { name, run } of runs) { + void this.runBackgroundProjection(name, run); } } @@ -389,6 +474,10 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { this.toastService.success(`${name} projection ready! (${(result.computeTime / 1000).toFixed(1)}s)`, 4000); }); } catch (error: unknown) { + if (error instanceof ProjectionCancelledError) { + // Stopped on purpose (Cancel / Stop all / superseded); the status list shows it. + return; + } const raw = error instanceof Error ? error.message : String(error); console.error(`${name} projection failed:`, error); this.ngZone.run(() => { @@ -428,6 +517,10 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { } } + // Running background projections keep going for the dashboard, but can no + // longer be cancelled or retried from the (reset) wizard. + this.projectionService.detachAll(); + // Reset wizard state so it's ready for a new upload this.preprocessingService.resetState(); @@ -440,7 +533,7 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { // A6: jump from a review summary card back to the step that owns that setting // and scroll the relevant section into view. - editSetting(step: number, targetId: string): void { + editSetting(step: WizardStepId, targetId: string): void { this.preprocessingService.goToStepWithScroll(step, targetId); } @@ -451,48 +544,87 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { this.projectionService.clearBackgroundStatuses(); // Clear local state - this.backgroundProjections.clear(); + this.backgroundProjections = new Map(); + this.backgroundItems = []; this.resetProcessingState(); // Reset wizard state this.preprocessingService.resetState(); - this.preprocessingService.goToStep(0); + this.preprocessingService.goToStep('upload'); } } - getBackgroundProjectionsArray(): { method: string; status: string; progress: number; message: string }[] { - const result: { method: string; status: string; progress: number; message: string }[] = []; - this.backgroundProjections.forEach((value, key) => { - result.push({ method: key, ...value }); - }); - return result; + // ============================================================================ + // Background projections: cancel / stop all / retry + // ============================================================================ + + /** True while at least one background projection is computing. */ + get hasRunningBackground(): boolean { + return this.backgroundItems.some(item => item.status === 'running'); + } + + /** Id shared by an item's Cancel and Retry button (only one is shown at a time). */ + backgroundActionId(method: string): string { + return `step6-background-action-${method}`; + } + + /** Cancel one running background projection; focus moves to its Retry button. */ + cancelBackgroundProjection(method: string): void { + if (!this.projectionService.cancel(method)) { + return; + } + this.announcer.announce(`${backgroundMethodName(method)} cancelled. You can retry it.`); + this.focusElement(this.backgroundActionId(method)); + } + + /** Cancel every running background projection. */ + stopAllBackgroundProjections(): void { + const stopped = this.projectionService.terminateAllWorkers(); + if (stopped.length === 0) { + return; + } + const noun = stopped.length === 1 ? 'projection' : 'projections'; + this.announcer.announce(`Stopped ${stopped.length} background ${noun}.`); + this.focusElement(BACKGROUND_HEADING_ID); + } + + /** Re-run one cancelled or failed background projection with the same data and settings. */ + retryBackgroundProjection(method: string): void { + if (!this.projectionService.canRetry(method)) { + return; + } + const name = backgroundMethodName(method); + this.dismissedMethods.delete(method); + this.announcer.announce(`Retrying ${name}.`); + void this.runBackgroundProjection(name, () => this.projectionService.retry(method)); + this.focusElement(this.backgroundActionId(method)); + } + + /** Keep keyboard focus in the list after the pressed button was replaced or removed. */ + private focusElement(id: string): void { + this.detectChangesIfAlive(); + document.getElementById(id)?.focus(); } // ============================================================================ // A3: persistent status/error screen // ============================================================================ - /** Copy the latest background status into the local map and rebuild the - * degraded/partial issue list from any failed background projections. */ - private syncBackgroundStatus( - statusMap: Map - ): void { - this.backgroundProjections.clear(); - statusMap.forEach((status, method) => { - this.backgroundProjections.set(method, { - status: status.status, - progress: status.progress, - message: status.message, - error: status.error, - }); - }); - - this.partialIssues = []; - this.backgroundProjections.forEach((value, method) => { - if (value.status === 'error' && !this.dismissedMethods.has(method)) { - this.partialIssues.push(classifyBackgroundFailure(method, value.error || value.message)); - } - }); + /** Adopt the latest background statuses, rebuild the list rows and the + * degraded/partial issue list from any failed background projection. */ + private syncBackgroundStatus(statusMap: Map): void { + this.backgroundProjections = new Map(statusMap); + this.backgroundItems = Array.from(statusMap.entries()).map(([method, s]) => ({ + method, + name: backgroundMethodName(method), + status: s.status, + progress: s.progress, + message: s.message, + canRetry: this.projectionService.canRetry(method), + })); + this.partialIssues = Array.from(statusMap.entries()) + .filter(([method, s]) => s.status === 'error' && !this.dismissedMethods.has(method)) + .map(([method, s]) => classifyBackgroundFailure(method, s.error || s.message)); } /** True once processing succeeded but at least one background projection failed. */ @@ -507,62 +639,46 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { /** Method status bar (FastMap primary + enabled background methods). */ get methodStatuses(): MethodStatus[] { - const roleFor = (name: string): string => { - if (name.startsWith('FastMap')) return 'Primary'; - if (name === 'PCA') return 'Instant'; - if (name === 't-SNE' || name === 'Sammon') return 'Slow'; - return 'Background'; - }; - return this.enabledMethods.map(label => { - const isPrimary = label.startsWith('FastMap'); - // Background status keys are lowercase alphanumeric ids (e.g. "tsne"). - const key = label - .replace(' (Primary)', '') - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); - let status: MethodStatus['status']; - - if (isPrimary) { - if (this.blockingIssue) status = 'failed'; - else if (this.processingComplete) status = 'ready'; - else if (this.isProcessing) status = 'computing'; - else status = 'queued'; - } else { - const bg = this.backgroundProjections.get(key); - if (bg) { - if (bg.status === 'complete') status = 'ready'; - else if (bg.status === 'running') status = 'computing'; - else if (bg.status === 'error') status = 'failed'; - else status = 'queued'; - } else { - // Not tracked via the worker status stream (e.g. PCA runs instantly): - // assume ready once the primary run has finished, otherwise queued. - status = this.processingComplete ? 'ready' : 'queued'; - } - } - - const labels: Record = { - ready: 'Ready', - computing: 'Computing…', - queued: 'Queued', - failed: 'Failed', + const status = label.startsWith('FastMap') ? this.primaryChipStatus() : this.backgroundChipStatus(label); + return { + name: label.replace(' (Primary)', ''), + role: methodRole(label), + status, + statusLabel: CHIP_LABELS[status], }; - - return { name: label.replace(' (Primary)', ''), role: roleFor(label), status, statusLabel: labels[status] }; }); } - /** "Fix in Schritt N" — reuse the A6 jump/scroll logic and confirm with a toast. */ + private primaryChipStatus(): ChipStatus { + if (this.blockingIssue) return 'failed'; + if (this.processingComplete) return 'ready'; + if (this.isProcessing) return 'computing'; + return 'queued'; + } + + private backgroundChipStatus(label: string): ChipStatus { + // Background status keys are lowercase alphanumeric ids (e.g. "tsne"). + const key = label.toLowerCase().replace(/[^a-z0-9]/g, ''); + const bg = this.backgroundProjections.get(key); + if (bg) { + return BACKGROUND_CHIP_STATUS[bg.status]; + } + // Not tracked via the worker status stream (e.g. PCA runs instantly): + // assume ready once the primary run has finished, otherwise queued. + return this.processingComplete ? 'ready' : 'queued'; + } + + /** "Fix in step N" — reuse the A6 jump/scroll logic and confirm with a toast. */ fixInStep(issue: WizardIssue): void { - const stepNumber = issue.step + 1; - const stepName = STEP_NAMES[issue.step] ?? ''; + const number = stepNumber(issue.step); + const stepName = getStepInfo(issue.step).title; if (issue.anchorId) { this.preprocessingService.goToStepWithScroll(issue.step, issue.anchorId); } else { this.preprocessingService.goToStep(issue.step); } - this.toastService.info(`Opening Step ${stepNumber} · ${stepName}`, 2500); + this.toastService.info(`Opening Step ${number} · ${stepName}`, 2500); } /** Dismiss a partial (optional) issue so the degraded notice can be cleared. */ @@ -578,15 +694,6 @@ export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { return match ? match[1].toLowerCase() : null; } - toggleTechnical(id: string): void { - if (this.expandedTechnical.has(id)) this.expandedTechnical.delete(id); - else this.expandedTechnical.add(id); - } - - isTechnicalExpanded(id: string): boolean { - return this.expandedTechnical.has(id); - } - /** * Retry / reprocess, keeping all uploaded data and settings. Reachable from * both the blocking-error screen and the success screen. diff --git a/src/app/preprocessing-wizard/testing/preprocessing-test-helpers.ts b/src/app/preprocessing-wizard/testing/preprocessing-test-helpers.ts new file mode 100644 index 0000000..4fdffdd --- /dev/null +++ b/src/app/preprocessing-wizard/testing/preprocessing-test-helpers.ts @@ -0,0 +1,32 @@ +import type { Mocked } from 'vitest'; +import { createSpyObj } from '../../../testing/create-spy-obj'; +import { DataLoaderService } from '../../services/data-loader.service'; +import { DataProcessorService } from '../../services/data-processor'; +import { ToastService } from '../../services/toast.service'; +import { PreprocessingService } from '../services/preprocessing.service'; + +/** + * A PreprocessingService on stub collaborators, outside Angular DI. The data + * loader reports no existing dataset names; `dataProcessor` carries only the + * DataProcessorService members a spec relies on (none by default). + */ +export function createTestPreprocessingService(dataProcessor: object = {}): PreprocessingService { + const dataLoader = { getDataSetNames: () => [] as string[] } as unknown as DataLoaderService; + return new PreprocessingService(dataProcessor as DataProcessorService, dataLoader); +} + +/** A ToastService whose every public method is a mock. */ +export function createToastServiceSpy(): Mocked { + return createSpyObj('ToastService', [ + 'show', + 'showUndo', + 'success', + 'error', + 'info', + 'warning', + 'pause', + 'resume', + 'remove', + 'clear', + ]); +} diff --git a/src/app/services/data-processor.spec.ts b/src/app/services/data-processor.spec.ts new file mode 100644 index 0000000..81906a8 --- /dev/null +++ b/src/app/services/data-processor.spec.ts @@ -0,0 +1,138 @@ +import { DataProcessorService } from './data-processor'; +import { DatasetCollection } from '../shared/interfaces/dataset-collection'; + +interface PostedMessage { + type?: string; + requestId?: unknown; + [key: string]: unknown; +} + +/** Minimal in-memory stand-in for the Pyodide worker. */ +class FakeWorker { + static last: FakeWorker | null = null; + onmessage: ((ev: MessageEvent) => void) | null = null; + readonly posted: PostedMessage[] = []; + + constructor() { + FakeWorker.last = this; + } + + postMessage(message: PostedMessage): void { + this.posted.push(message); + } + + terminate(): void { + // no-op + } + + /** Deliver a reply from the "worker" to the main thread. */ + reply(data: unknown): void { + this.onmessage?.({ data } as MessageEvent); + } +} + +/** + * Concurrent processWithConfig() runs all answer with type 'processed', so + * replies must be matched to their request by a per-request id rather than by + * type, or concurrent requests resolve/reject with each other's replies. + */ +describe('DataProcessorService – request/reply matching', () => { + const globals = globalThis as unknown as { Worker: unknown }; + let originalWorker: unknown; + let service: DataProcessorService; + let worker: FakeWorker; + + const datasetA = { tag: 'A' } as unknown as DatasetCollection; + const datasetB = { tag: 'B' } as unknown as DatasetCollection; + + beforeEach(() => { + originalWorker = globals.Worker; + globals.Worker = FakeWorker; + vi.spyOn(console, 'warn').mockImplementation(() => undefined); // the service logs worker errors + service = new DataProcessorService(); + if (!FakeWorker.last) { + throw new Error('FakeWorker was not constructed'); + } + worker = FakeWorker.last; + }); + + afterEach(() => { + globals.Worker = originalWorker; + FakeWorker.last = null; + }); + + function startTwoRequests(): [Promise, Promise, unknown, unknown] { + const a = service.processWithConfig('a.csv', {}); + const b = service.processWithConfig('b.csv', {}); + expect(worker.posted.length).toBe(2); + const [idA, idB] = worker.posted.map(m => m.requestId); + expect(idA).toBeDefined(); + expect(idA).not.toEqual(idB); + return [a, b, idA, idB]; + } + + it('resolves two concurrent requests with their own replies', async () => { + const [a, b, idA, idB] = startTwoRequests(); + + worker.reply({ type: 'processed', dataset: datasetB, requestId: idB }); + worker.reply({ type: 'processed', dataset: datasetA, requestId: idA }); + + expect(await a).toBe(datasetA); + expect(await b).toBe(datasetB); + }); + + it('rejects only the request an error reply belongs to', async () => { + const [a, b, idA, idB] = startTwoRequests(); + + worker.reply({ type: 'error', message: 'boom', requestId: idA }); + await expect(a).rejects.toThrowError('boom'); + + worker.reply({ type: 'processed', dataset: datasetB, requestId: idB }); + expect(await b).toBe(datasetB); + }); + + describe('pruneFiles', () => { + /** Resolves to true once `promise` has settled, false while it is still pending. */ + async function isSettled(promise: Promise): Promise { + let settled = false; + promise.then( + () => (settled = true), + () => (settled = true) + ); + await Promise.resolve(); + await Promise.resolve(); + return settled; + } + + it('sends the keep list to the worker and resolves on its filesPruned reply', async () => { + const pruned = service.pruneFiles(['current.csv', 'undo.csv']); + + expect(worker.posted.length).toBe(1); + const [request] = worker.posted; + expect(request.type).toBe('pruneFiles'); + expect(request['keep']).toEqual(['current.csv', 'undo.csv']); + + worker.reply({ type: 'filesPruned', removed: ['old.csv'], requestId: request.requestId }); + await expect(pruned).resolves.toEqual(undefined); + }); + + it('ignores a filesPruned reply that belongs to another request', async () => { + const pruned = service.pruneFiles([]); + const [request] = worker.posted; + + worker.reply({ type: 'filesPruned', removed: [], requestId: -1 }); + expect(await isSettled(pruned)).toBe(false); + + worker.reply({ type: 'filesPruned', removed: [], requestId: request.requestId }); + await expect(pruned).resolves.not.toThrow(); + }); + + it('rejects when the worker reports an error for the prune request', async () => { + const pruned = service.pruneFiles(['a.csv']); + const [request] = worker.posted; + + worker.reply({ type: 'error', message: 'unlink failed', requestId: request.requestId }); + await expect(pruned).rejects.toThrowError('unlink failed'); + }); + }); +}); diff --git a/src/app/services/data-processor.ts b/src/app/services/data-processor.ts index 49ee8a6..d671e4b 100644 --- a/src/app/services/data-processor.ts +++ b/src/app/services/data-processor.ts @@ -3,6 +3,12 @@ import { DatasetCollection } from '../shared/interfaces/dataset-collection'; import { BehaviorSubject, Observable, Subject, of } from 'rxjs'; import { WorkerReply } from '../shared/interfaces/pyodide-messages'; +/** + * A worker reply as received on the main thread. Replies to a request sent via + * sendRequestUntil() echo that request's `requestId` (see pyodide.worker.ts). + */ +type TaggedWorkerReply = WorkerReply & { requestId?: number }; + @Injectable({ providedIn: 'root' }) export class DataProcessorService { private worker = new Worker(new URL('../workers/pyodide.worker', import.meta.url), { type: 'module' }); @@ -10,7 +16,10 @@ export class DataProcessorService { private thumbCache = new Map(); private thumbSubjects = new Map>(); - private message$ = new Subject(); + private message$ = new Subject(); + // Monotonic id attached to every request so its reply can be matched even when + // several requests expecting the same reply type are in flight. + private nextRequestId = 0; private processingProgress$ = new Subject<{ step: string; progress: number; message: string }>(); constructor() { @@ -44,15 +53,6 @@ export class DataProcessorService { return result.folder; } - async process(file: File): Promise { - const result = await this.sendRequestUntil('processed', { - type: 'process', - fileName: file.name, - buffer: await file.arrayBuffer(), - }); - return result.dataset; - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- returns dynamically typed JSON from the worker filesystem async fetchJson(file: string): Promise { const result = await this.sendRequestUntil( @@ -142,6 +142,18 @@ export class DataProcessorService { return result.data; } + /** + * Delete uploaded input files (and their derived result files) from the + * worker's virtual FS, except those listed in `keep`. The caller passes every + * file name still referenced by the current state and the undo/redo history. + */ + async pruneFiles(keep: string[]): Promise { + await this.sendRequestUntil('filesPruned', { + type: 'pruneFiles', + keep, + }); + } + /** * Observable for processing progress updates */ @@ -197,7 +209,11 @@ export class DataProcessorService { } /** - * Utility to wait for a specific message type (optionally filtered) + * Send a request and wait for its reply of the given type (optionally filtered). + * Replies are matched by a per-request id that the worker echoes back, so + * concurrent requests of the same reply type (e.g. two processWithConfig runs, + * which both answer with 'processed') never resolve or reject with each other's + * replies. */ private sendRequestUntil( type: T['type'], @@ -206,6 +222,7 @@ export class DataProcessorService { matchFn: (msg: T) => boolean = () => true, timeoutMs = 60000 ): Promise { + const requestId = ++this.nextRequestId; return new Promise((resolve, reject) => { const timer = setTimeout(() => { sub.unsubscribe(); @@ -213,6 +230,9 @@ export class DataProcessorService { }, timeoutMs); const sub = this.message$.subscribe(msg => { + if (msg.requestId !== requestId) { + return; + } if (msg.type === 'error') { clearTimeout(timer); sub.unsubscribe(); @@ -227,7 +247,7 @@ export class DataProcessorService { } }); - this.worker.postMessage(message); + this.worker.postMessage({ ...message, requestId }); }); } } diff --git a/src/app/services/dataset-collection.service.spec.ts b/src/app/services/dataset-collection.service.spec.ts index d9d41ab..49d56f5 100644 --- a/src/app/services/dataset-collection.service.spec.ts +++ b/src/app/services/dataset-collection.service.spec.ts @@ -93,16 +93,19 @@ describe('DatasetCollectionService', () => { expect(positions).toContain('tsne'); }); - it('should emit on observable when collection changes', (done: DoneFn) => { + it('should emit on observable when collection changes', async () => { let emitCount = 0; - service.dataSetCollectionSubject$.subscribe(() => { - emitCount++; - if (emitCount === 2) { - // Initial + one setDatasetCollection - done(); - } - }); + const secondEmission = new Promise(resolve => + service.dataSetCollectionSubject$.subscribe(() => { + emitCount++; + if (emitCount === 2) { + // Initial + one setDatasetCollection + resolve(); + } + }) + ); service.setDatasetCollection([makeEntry('new-ds', 't1')]); + await secondEmission; }); }); diff --git a/src/app/services/filter.service.spec.ts b/src/app/services/filter.service.spec.ts index 62ecb26..588d394 100644 --- a/src/app/services/filter.service.spec.ts +++ b/src/app/services/filter.service.spec.ts @@ -1,3 +1,4 @@ +import { firstValueFrom } from 'rxjs'; import { FilterService } from './filter.service'; import { GlyphObject } from '../glyph/glyph-object'; import { FilterMode } from '../shared/enum/filter-mode'; @@ -50,15 +51,14 @@ describe('FilterService', () => { expect(service.getFilters().length).toBe(0); }); - it('should emit filterChanged$ when refreshFilters is called', (done: DoneFn) => { + it('should emit filterChanged$ when refreshFilters is called', async () => { const glyphMap = createGlyphMap(['1', '2', '3']); service.setActiveGlyphData(glyphMap); - service.filterChanged$.subscribe(() => { - done(); - }); + const emitted = firstValueFrom(service.filterChanged$); service.refreshFilters(); + await emitted; }); it('should mark no items as passive when no filters exist', () => { @@ -67,7 +67,7 @@ describe('FilterService', () => { service.refreshFilters(); glyphMap.forEach(glyph => { - expect(glyph.passive).toBeFalse(); + expect(glyph.passive).toBe(false); }); expect(service.filteredItems).toBe(3); }); @@ -84,9 +84,9 @@ describe('FilterService', () => { service.ensureFilter(filter); service.refreshFilters(); - expect(glyphMap.get('1')?.passive).toBeFalse(); - expect(glyphMap.get('2')?.passive).toBeFalse(); - expect(glyphMap.get('3')?.passive).toBeTrue(); + expect(glyphMap.get('1')?.passive).toBe(false); + expect(glyphMap.get('2')?.passive).toBe(false); + expect(glyphMap.get('3')?.passive).toBe(true); expect(service.filteredItems).toBe(2); }); @@ -101,9 +101,9 @@ describe('FilterService', () => { service.ensureFilter(filter); service.refreshFilters(); - expect(glyphMap.get('1')?.passive).toBeFalse(); - expect(glyphMap.get('2')?.passive).toBeTrue(); - expect(glyphMap.get('3')?.passive).toBeTrue(); + expect(glyphMap.get('1')?.passive).toBe(false); + expect(glyphMap.get('2')?.passive).toBe(true); + expect(glyphMap.get('3')?.passive).toBe(true); expect(service.filteredItems).toBe(1); }); @@ -128,7 +128,7 @@ describe('FilterService', () => { // After clearing, all items should be active glyphMap.forEach(glyph => { - expect(glyph.passive).toBeFalse(); + expect(glyph.passive).toBe(false); }); }); @@ -142,7 +142,7 @@ describe('FilterService', () => { service.refreshFilters(); glyphMap.forEach(glyph => { - expect(glyph.passive).toBeFalse(); + expect(glyph.passive).toBe(false); }); expect(service.filteredItems).toBe(2); }); diff --git a/src/app/services/projection.service.spec.ts b/src/app/services/projection.service.spec.ts new file mode 100644 index 0000000..f7a520e --- /dev/null +++ b/src/app/services/projection.service.spec.ts @@ -0,0 +1,303 @@ +import { TestBed } from '@angular/core/testing'; +import { ProjectionCancelledError, ProjectionService } from './projection.service'; +import { ProjectionWorkerRequest, ProjectionWorkerResponse } from '../shared/types/projection.types'; + +/** Minimal in-memory stand-in for a projection worker. */ +class FakeWorker { + static instances: FakeWorker[] = []; + onmessage: ((ev: MessageEvent) => void) | null = null; + onerror: ((ev: ErrorEvent) => void) | null = null; + readonly posted: ProjectionWorkerRequest[] = []; + terminated = false; + + constructor() { + FakeWorker.instances.push(this); + } + + postMessage(message: ProjectionWorkerRequest): void { + this.posted.push(message); + } + + terminate(): void { + this.terminated = true; + } + + /** Deliver a message from the "worker" to the main thread. */ + emit(data: ProjectionWorkerResponse): void { + this.onmessage?.({ data } as MessageEvent); + } + + /** Raise an uncaught worker error. */ + crash(message: string): void { + this.onerror?.({ message } as ErrorEvent); + } +} + +describe('ProjectionService – background projections', () => { + const globals = globalThis as unknown as { Worker: unknown }; + let originalWorker: unknown; + let service: ProjectionService; + + const features = [ + [1, 2], + [3, 4], + ]; + const ids = ['a', 'b']; + const positions = [ + { id: 'a', x: 0, y: 1 }, + { id: 'b', x: 2, y: 3 }, + ]; + + beforeEach(() => { + originalWorker = globals.Worker; + globals.Worker = FakeWorker; + FakeWorker.instances = []; + TestBed.configureTestingModule({}); + service = TestBed.inject(ProjectionService); + }); + + afterEach(() => { + globals.Worker = originalWorker; + FakeWorker.instances = []; + }); + + function lastWorker(): FakeWorker { + const worker = FakeWorker.instances[FakeWorker.instances.length - 1]; + if (!worker) { + throw new Error('No worker was started'); + } + return worker; + } + + function statusOf(method: string): string | undefined { + return service.getBackgroundStatus(method)?.status; + } + + function startUmap(): Promise { + return service.runUMAP(features, ids, { neighbors: 15, minDist: 0.1 }); + } + + it('computes in a worker, reports progress and resolves with the result', async () => { + const run = service.runMDS(features, ids); + const worker = lastWorker(); + expect(worker.posted[0]).toEqual(expect.objectContaining({ type: 'compute', method: 'mds', features, ids })); + expect(statusOf('mds')).toBe('running'); + + worker.emit({ type: 'progress', progress: 40, message: 'Iterating' }); + expect(service.getBackgroundStatus('mds')?.progress).toBe(40); + + worker.emit({ type: 'result', positions, computeTime: 12 }); + expect(await run).toEqual({ method: 'mds', positions, computeTime: 12 }); + expect(statusOf('mds')).toBe('complete'); + expect(worker.terminated).toBe(true); + expect(service.isRunning('mds')).toBe(false); + expect(service.canRetry('mds')).toBe(false); + }); + + describe('cancel', () => { + it('terminates the worker, rejects the run and marks the method cancelled', async () => { + const run = startUmap(); + const worker = lastWorker(); + + expect(service.cancel('umap')).toBe(true); + + await expect(run).rejects.toEqual(expect.any(ProjectionCancelledError)); + expect(worker.terminated).toBe(true); + expect(statusOf('umap')).toBe('cancelled'); + expect(service.isRunning('umap')).toBe(false); + expect(service.canRetry('umap')).toBe(true); + }); + + it('returns false and changes nothing for a method that is not running', async () => { + const run = service.runMDS(features, ids); + lastWorker().emit({ type: 'result', positions, computeTime: 1 }); + await run; + + expect(service.cancel('mds')).toBe(false); + expect(service.cancel('tsne')).toBe(false); + expect(statusOf('mds')).toBe('complete'); + expect(service.getBackgroundStatus('tsne')).toBeUndefined(); + }); + + it('ignores late messages from a cancelled worker', async () => { + const run = startUmap(); + const worker = lastWorker(); + service.cancel('umap'); + await expect(run).rejects.toThrow(); + + worker.emit({ type: 'progress', progress: 90 }); + worker.emit({ type: 'result', positions, computeTime: 5 }); + + expect(statusOf('umap')).toBe('cancelled'); + expect(service.canRetry('umap')).toBe(true); + }); + + it('leaves the other running methods alone', () => { + startUmap().catch(() => undefined); + const tsne = service.runTSNE(features, ids, { perplexity: 30, iterations: 100 }); + + service.cancel('umap'); + + expect(service.isRunning('tsne')).toBe(true); + expect(statusOf('tsne')).toBe('running'); + tsne.catch(() => undefined); + }); + }); + + describe('terminateAllWorkers', () => { + it('cancels every running method and reports which ones were stopped', async () => { + const umap = startUmap(); + const tsne = service.runTSNE(features, ids, { perplexity: 30, iterations: 100 }); + const mds = service.runMDS(features, ids); + FakeWorker.instances[2].emit({ type: 'result', positions, computeTime: 1 }); + await mds; + + const stopped = service.terminateAllWorkers(); + + expect(stopped.sort()).toEqual(['tsne', 'umap']); + expect(FakeWorker.instances.every(w => w.terminated)).toBe(true); + expect(statusOf('umap')).toBe('cancelled'); + expect(statusOf('tsne')).toBe('cancelled'); + expect(statusOf('mds')).toBe('complete'); + await expect(umap).rejects.toEqual(expect.any(ProjectionCancelledError)); + await expect(tsne).rejects.toEqual(expect.any(ProjectionCancelledError)); + }); + + it('returns an empty list when nothing is running', () => { + expect(service.terminateAllWorkers()).toEqual([]); + }); + }); + + describe('retry', () => { + it('re-runs a cancelled method with the same data and settings in a new worker', async () => { + startUmap().catch(() => undefined); + const first = lastWorker(); + service.cancel('umap'); + + const retried = service.retry('umap'); + const second = lastWorker(); + + expect(second).not.toBe(first); + expect(second.posted[0]).toEqual(first.posted[0]); + expect(statusOf('umap')).toBe('running'); + expect(service.canRetry('umap')).toBe(false); + + second.emit({ type: 'result', positions, computeTime: 7 }); + expect(await retried).toEqual({ method: 'umap', positions, computeTime: 7 }); + expect(statusOf('umap')).toBe('complete'); + expect(service.canRetry('umap')).toBe(false); + }); + + it('re-runs a method whose worker reported an error', async () => { + const run = service.runLLE(features, ids, { neighbors: 5 }); + lastWorker().emit({ type: 'error', error: 'singular matrix' }); + await expect(run).rejects.toThrowError('lle failed: singular matrix'); + expect(statusOf('lle')).toBe('error'); + expect(service.canRetry('lle')).toBe(true); + + const retried = service.retry('lle'); + expect(lastWorker().posted[0].config).toEqual({ lleNeighbors: 5 }); + lastWorker().emit({ type: 'result', positions, computeTime: 3 }); + await expect(retried).resolves.not.toThrow(); + }); + + it('re-runs a method whose worker crashed', async () => { + const run = service.runSammon(features, ids); + lastWorker().crash('out of memory'); + await expect(run).rejects.toThrowError('sammon worker error: out of memory'); + expect(statusOf('sammon')).toBe('error'); + + expect(service.canRetry('sammon')).toBe(true); + service.retry('sammon').catch(() => undefined); + expect(statusOf('sammon')).toBe('running'); + }); + + it('rejects when there is no previous run of the method', async () => { + await expect(service.retry('tsne')).rejects.toThrowError('There is no previous tsne run to retry'); + }); + + it('forgets retryable runs when the statuses are cleared or the jobs are detached', () => { + startUmap().catch(() => undefined); + service.runMDS(features, ids).catch(() => undefined); + service.terminateAllWorkers(); + + service.detachAll(); + expect(service.canRetry('umap')).toBe(false); + + service.runTopoMap(features, ids).catch(() => undefined); + service.cancel('topomap'); + service.clearBackgroundStatuses(); + expect(service.canRetry('topomap')).toBe(false); + expect(service.getBackgroundStatus('topomap')).toBeUndefined(); + }); + }); + + describe('runs of an earlier dataset', () => { + it('keeps an earlier run of the same method computing; only the newer one is controllable', async () => { + const earlier = startUmap(); + const earlierWorker = lastWorker(); + const newer = startUmap(); + const newerWorker = lastWorker(); + expect(earlierWorker.terminated).toBe(false); + + expect(service.cancel('umap')).toBe(true); + await expect(newer).rejects.toEqual(expect.any(ProjectionCancelledError)); + expect(newerWorker.terminated).toBe(true); + + earlierWorker.emit({ type: 'result', positions, computeTime: 4 }); + expect(await earlier).toEqual({ method: 'umap', positions, computeTime: 4 }); + expect(earlierWorker.terminated).toBe(true); + // The newer run was cancelled, so it stays retryable. + expect(service.canRetry('umap')).toBe(true); + }); + + it('lets detached runs finish and report, but no longer cancels or retries them', async () => { + const run = startUmap(); + const worker = lastWorker(); + + service.detachAll(); + + expect(service.isRunning('umap')).toBe(false); + expect(service.cancel('umap')).toBe(false); + expect(service.terminateAllWorkers()).toEqual([]); + expect(worker.terminated).toBe(false); + + worker.emit({ type: 'progress', progress: 70 }); + expect(service.getBackgroundStatus('umap')?.progress).toBe(70); + + worker.emit({ type: 'result', positions, computeTime: 9 }); + expect(await run).toEqual({ method: 'umap', positions, computeTime: 9 }); + expect(statusOf('umap')).toBe('complete'); + expect(service.canRetry('umap')).toBe(false); + }); + }); +}); + +describe('ProjectionService – primary FastMap on the main thread', () => { + let service: ProjectionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ProjectionService); + }); + + it('loads DruidJS on demand and projects every row to finite 2-D coordinates', async () => { + const features = Array.from({ length: 40 }, (_, i) => [Math.sin(i), Math.cos(i / 3), (i % 7) / 7, i / 40]); + const ids = features.map((_, i) => `row-${i}`); + const workerSpy = vi.spyOn(globalThis, 'Worker'); + + const result = await service.runFastMapSync(features, ids); + + expect(result.method).toBe('fastmap'); + expect(result.positions.map(p => p.id)).toEqual(ids); + expect(result.positions.every(p => Number.isFinite(p.x) && Number.isFinite(p.y))).toBe(true); + expect(Number.isInteger(result.computeTime)).toBe(true); + expect(workerSpy).not.toHaveBeenCalled(); + }); + + it('reports a DruidJS failure as a FastMap error', async () => { + const ragged = [[1, 2], [3]]; + + await expect(service.runFastMapSync(ragged, ['a', 'b'])).rejects.toThrowError(/^FastMap failed: /); + }); +}); diff --git a/src/app/services/projection.service.ts b/src/app/services/projection.service.ts index ce8ca7a..7d5d994 100644 --- a/src/app/services/projection.service.ts +++ b/src/app/services/projection.service.ts @@ -1,124 +1,102 @@ import { Injectable, NgZone } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; -import * as druid from '@saehrimnir/druidjs'; +import { DruidModule, druidSpecFor, runDruidProjection } from '../workers/druid-runner'; import { + ProjectionComputeConfig, ProjectionMethod, ProjectionResult, ProjectionWorkerRequest, ProjectionWorkerResponse, } from '../shared/types/projection.types'; -// Re-export types for backward compatibility -export type { - ProjectionMethod, - ProjectionResult, - ProjectionComputeConfig, - ProjectionWorkerRequest, - ProjectionWorkerResponse, -} from '../shared/types/projection.types'; +// Re-exported for consumers that import it alongside ProjectionService +export type { ProjectionResult } from '../shared/types/projection.types'; export interface BackgroundStatus { method: string; - status: 'pending' | 'running' | 'complete' | 'error'; + status: 'pending' | 'running' | 'complete' | 'error' | 'cancelled'; progress: number; message: string; error?: string; } +/** Rejection reason of a background projection that was stopped on purpose (cancel / terminateAllWorkers). */ +export class ProjectionCancelledError extends Error { + constructor(readonly method: string) { + super(`${method} was cancelled`); + this.name = 'ProjectionCancelledError'; + } +} + +/** Everything needed to start, or re-start, one background projection. */ +interface ProjectionRequest { + method: ProjectionMethod; + features: number[][]; + ids: (string | number)[]; + config?: ProjectionComputeConfig; +} + +/** One background projection computing in its own worker. */ +interface ProjectionJob { + worker: Worker; + request: ProjectionRequest; + reject: (reason: Error) => void; + /** Set once the job finished or was cancelled; later worker messages are ignored. */ + ended: boolean; +} + +/** + * Loads DruidJS on first use. A dynamic import keeps the library (with its + * inlined WASM kernels) out of the initial bundle; only FastMap needs it on the + * main thread, every other method loads it inside its worker. + */ +function loadDruid(): Promise { + return import('@saehrimnir/druidjs'); +} + +/** + * Computes 2D projections. The primary FastMap projection runs synchronously on + * the main thread (runFastMapSync) and cannot be interrupted; every optional + * method runs in its own Web Worker, so it can be cancelled (cancel / + * terminateAllWorkers) and re-run with the same data and settings (retry). + */ @Injectable({ providedIn: 'root', }) export class ProjectionService { private backgroundStatus$ = new BehaviorSubject>(new Map()); - private workers = new Map(); - - constructor(private ngZone: NgZone) {} - /** - * Run PCA (fast, blocking) - runs synchronously on main thread + * The controllable job per method: the latest one started and not yet + * detached. An earlier job of the same method, or a job released by + * detachAll(), keeps computing and reporting on its own. + */ + private jobs = new Map(); + /** + * The latest request per method, kept for retry() until that method completes + * or detachAll() is called. All entries of one run share the same feature + * matrix, so this keeps at most one matrix alive. */ - async runPCA(features: number[][], ids: (string | number)[]): Promise { - const startTime = Date.now(); + private requests = new Map(); - try { - // DruidJS PCA: new druid.PCA(data, components).transform() - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DruidJS type definitions are incomplete - const pca = new (druid as any).PCA(features, 2); // 2 components for 2D visualization - const embedding = pca.transform(); - - const positions = embedding.map((coords: number[], i: number) => ({ - id: ids[i], - x: coords[0], - y: coords[1], - })); - - return { - method: 'pca', - positions, - computeTime: Date.now() - startTime, - }; - } catch (error: unknown) { - throw new Error(`PCA failed: ${error instanceof Error ? error.message : String(error)}`); - } - } + constructor(private ngZone: NgZone) {} /** * Run FastMap (blocking) - PRIMARY projection for dashboard * Fast distance-preserving projection with O(n) complexity * Ideal for large datasets (40K+ items) + * + * DruidJS is loaded on the first call (see loadDruid), then FastMap runs + * synchronously on the main thread. computeTime includes that load, in whole ms. */ async runFastMapSync(features: number[][], ids: (string | number)[]): Promise { - const startTime = Date.now(); - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DruidJS type definitions are incomplete - const fastmap = new (druid as any).FASTMAP(features, 2); - const embedding = fastmap.transform(); - - const positions = embedding.map((coords: number[], i: number) => ({ - id: ids[i], - x: coords[0], - y: coords[1], - })); - - return { - method: 'fastmap', - positions, - computeTime: Date.now() - startTime, - }; + const { positions, computeTime } = await runDruidProjection(loadDruid, druidSpecFor('fastmap'), features, ids); + return { method: 'fastmap', positions, computeTime: Math.round(computeTime) }; } catch (error: unknown) { throw new Error(`FastMap failed: ${error instanceof Error ? error.message : String(error)}`); } } - /** - * Run IsoMap (blocking) - Non-linear manifold learning - * Preserves geodesic distances - slower than FastMap but better structure preservation - */ - async runIsoMapSync(features: number[][], ids: (string | number)[]): Promise { - const startTime = Date.now(); - - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DruidJS type definitions are incomplete - const isomap = new (druid as any).ISOMAP(features, 2); - const embedding = isomap.transform(); - - const positions = embedding.map((coords: number[], i: number) => ({ - id: ids[i], - x: coords[0], - y: coords[1], - })); - - return { - method: 'isomap', - positions, - computeTime: Date.now() - startTime, - }; - } catch (error: unknown) { - throw new Error(`IsoMap failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - /** * Run FastMap (background) - runs in Web Worker */ @@ -204,19 +182,10 @@ export class ProjectionService { } /** - * Run TriMap (background) - Good for large datasets + * Run TriMap (background) - Good for large datasets. Uses DruidJS's defaults. */ - async runTriMap( - features: number[][], - ids: (string | number)[], - config?: { weightAdj?: number } - ): Promise { - return this.runProjectionInWorker( - 'trimap', - features, - ids, - config ? { trimapWeightAdj: config.weightAdj } : undefined - ); + async runTriMap(features: number[][], ids: (string | number)[]): Promise { + return this.runProjectionInWorker('trimap', features, ids); } /** @@ -236,77 +205,139 @@ export class ProjectionService { /** * Run a projection in a Web Worker (non-blocking) */ - private async runProjectionInWorker( + private runProjectionInWorker( method: ProjectionMethod, features: number[][], ids: (string | number)[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- config varies per projection method - config?: any + config?: ProjectionComputeConfig ): Promise { + return this.startJob({ method, features, ids, config }); + } + + /** + * Start one background projection in a fresh worker; it becomes the method's + * controllable job. A still-running earlier job of the same method is not + * stopped (it may belong to a dataset already handed to the dashboard): it + * keeps computing, reports its status and settles its own promise. + */ + private startJob(request: ProjectionRequest): Promise { + const { method } = request; + this.requests.set(method, request); + return new Promise((resolve, reject) => { - // Create worker for this projection const worker = new Worker(new URL('../workers/projection.worker', import.meta.url), { type: 'module', }); - - this.workers.set(method, worker); + const job: ProjectionJob = { worker, request, reject, ended: false }; + this.jobs.set(method, job); this.updateBackgroundStatus(method, 'running', 0, `Starting ${method.toUpperCase()}...`); - // Handle messages from worker worker.onmessage = ({ data }: MessageEvent) => { - this.ngZone.run(() => { - if (data.type === 'progress') { - // Update progress status - this.updateBackgroundStatus( - method, - 'running', - data.progress || 0, - data.message || `Computing ${method}...` - ); - } else if (data.type === 'result') { - // Computation complete - this.updateBackgroundStatus(method, 'complete', 100, `${method} complete`); - worker.terminate(); - this.workers.delete(method); - resolve({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ProjectionMethod type narrowing not available here - method: method as any, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed present when data.type === 'result' - positions: data.positions!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed present when data.type === 'result' - computeTime: data.computeTime!, - }); - } else if (data.type === 'error') { - // Error occurred - this.updateBackgroundStatus(method, 'error', 0, `${method} failed`, data.error); - worker.terminate(); - this.workers.delete(method); - reject(new Error(`${method} failed: ${data.error}`)); - } - }); + this.ngZone.run(() => this.handleWorkerMessage(job, data, resolve)); }; - - worker.onerror = error => { - this.ngZone.run(() => { - this.updateBackgroundStatus(method, 'error', 0, `${method} worker error`, error.message); - worker.terminate(); - this.workers.delete(method); - reject(new Error(`${method} worker error: ${error.message}`)); - }); + worker.onerror = (event: ErrorEvent) => { + this.ngZone.run(() => this.failJob(job, `${method} worker error`, event.message)); }; - // Send computation request to worker - const request: ProjectionWorkerRequest = { + const message: ProjectionWorkerRequest = { type: 'compute', method, - features, - ids, - config, + features: request.features, + ids: request.ids, + config: request.config, }; - worker.postMessage(request); + worker.postMessage(message); }); } + private handleWorkerMessage( + job: ProjectionJob, + data: ProjectionWorkerResponse, + resolve: (result: ProjectionResult) => void + ): void { + const { method } = job.request; + if (job.ended) { + return; // a late message from a cancelled worker + } + if (data.type === 'progress') { + this.updateBackgroundStatus(method, 'running', data.progress || 0, data.message || `Computing ${method}...`); + } else if (data.type === 'result') { + this.endJob(job); + if (this.requests.get(method) === job.request) { + this.requests.delete(method); + } + this.updateBackgroundStatus(method, 'complete', 100, `${method} complete`); + resolve({ method, positions: data.positions ?? [], computeTime: data.computeTime ?? 0 }); + } else if (data.type === 'error') { + this.failJob(job, `${method} failed`, data.error ?? 'Unknown error'); + } + } + + /** The worker reported an error: end the job, mark the method failed (retryable) and reject. */ + private failJob(job: ProjectionJob, summary: string, detail: string): void { + if (job.ended) { + return; + } + this.endJob(job); + this.updateBackgroundStatus(job.request.method, 'error', 0, summary, detail); + job.reject(new Error(`${summary}: ${detail}`)); + } + + /** Terminate the job's worker and, if it is still the method's controllable job, forget it. */ + private endJob(job: ProjectionJob): void { + job.ended = true; + job.worker.terminate(); + if (this.jobs.get(job.request.method) === job) { + this.jobs.delete(job.request.method); + } + } + + /** + * Cancel the running background projection of `method`: terminate its worker, + * reject its pending promise with ProjectionCancelledError and mark its status + * 'cancelled'. The request is kept, so retry() can run it again. Returns false + * when the method has no controllable running job. + */ + cancel(method: string): boolean { + const job = this.jobs.get(method); + if (!job) { + return false; + } + this.endJob(job); + job.reject(new ProjectionCancelledError(method)); + this.updateBackgroundStatus(method, 'cancelled', 0, 'Cancelled'); + return true; + } + + /** True while a controllable background projection of `method` is computing. */ + isRunning(method: string): boolean { + return this.jobs.has(method); + } + + /** True when retry() can re-run `method`: its last run was cancelled or failed. */ + canRetry(method: string): boolean { + return !this.jobs.has(method) && this.requests.has(method); + } + + /** Re-run the last (cancelled or failed) request of `method` with the same data and settings. */ + retry(method: string): Promise { + const request = this.requests.get(method); + if (!request) { + return Promise.reject(new Error(`There is no previous ${method} run to retry`)); + } + return this.startJob(request); + } + + /** + * Hand every running projection over, e.g. once the wizard passes its dataset + * to the dashboard or starts on a new one: the jobs keep computing and + * reporting their status, but can no longer be cancelled or retried. + */ + detachAll(): void { + this.jobs.clear(); + this.requests.clear(); + } + /** * Parse CSV features exported from Python * Format: ID,feature1,feature2,... @@ -399,19 +430,18 @@ export class ProjectionService { } /** - * Clear all background statuses + * Clear all background statuses (and the requests kept for retry) */ clearBackgroundStatuses(): void { + this.requests.clear(); this.backgroundStatus$.next(new Map()); } /** - * Terminate all running workers + * Cancel every running background projection (see cancel()); each is marked + * 'cancelled'. Returns the methods that were stopped. */ - terminateAllWorkers(): void { - this.workers.forEach((worker, _method) => { - worker.terminate(); - }); - this.workers.clear(); + terminateAllWorkers(): string[] { + return Array.from(this.jobs.keys()).filter(method => this.cancel(method)); } } diff --git a/src/app/services/toast.service.spec.ts b/src/app/services/toast.service.spec.ts index 2825547..ecbcfb8 100644 --- a/src/app/services/toast.service.spec.ts +++ b/src/app/services/toast.service.spec.ts @@ -1,4 +1,5 @@ -import { ToastService, Toast } from './toast.service'; +import { firstValueFrom } from 'rxjs'; +import { ToastService, Toast, MIN_ACTION_TOAST_MS } from './toast.service'; describe('ToastService', () => { let service: ToastService; @@ -7,11 +8,9 @@ describe('ToastService', () => { service = new ToastService(); }); - it('should start with no toasts', (done: DoneFn) => { - service.toasts$.subscribe(toasts => { - expect(toasts.length).toBe(0); - done(); - }); + it('should start with no toasts', async () => { + const toasts = await firstValueFrom(service.toasts$); + expect(toasts.length).toBe(0); }); it('should add a toast via show()', () => { @@ -24,30 +23,18 @@ describe('ToastService', () => { expect(latest[0].type).toBe('info'); }); - it('should add success toast', () => { - service.success('Done', 0); - let latest: Toast[] = []; - service.toasts$.subscribe(t => (latest = t)); - - expect(latest.length).toBe(1); - expect(latest[0].type).toBe('success'); - }); - - it('should add error toast', () => { - service.error('Failed', 0); + it.each([ + ['success', 'Done'], + ['error', 'Failed'], + ['warning', 'Careful'], + ] as const)('should add %s toast', (type, message) => { + service[type](message, 0); let latest: Toast[] = []; service.toasts$.subscribe(t => (latest = t)); expect(latest.length).toBe(1); - expect(latest[0].type).toBe('error'); - }); - - it('should add warning toast', () => { - service.warning('Careful', 0); - let latest: Toast[] = []; - service.toasts$.subscribe(t => (latest = t)); - - expect(latest[0].type).toBe('warning'); + expect(latest[0].type).toBe(type); + expect(latest[0].message).toBe(message); }); it('should assign unique IDs', () => { @@ -82,16 +69,133 @@ describe('ToastService', () => { expect(latest.length).toBe(0); }); - it('should auto-remove toast after duration', (done: DoneFn) => { - service.show('Temp', 'info', 50); // 50ms duration - let latest: Toast[] = []; - service.toasts$.subscribe(t => (latest = t)); + describe('showUndo', () => { + function latestToasts(): Toast[] { + let latest: Toast[] = []; + service.toasts$.subscribe(t => (latest = t)).unsubscribe(); + return latest; + } + + it('adds an info toast with an English "Undo" action', () => { + service.showUndo('File replaced', () => true, 0); + const [toast] = latestToasts(); + expect(toast.message).toBe('File replaced'); + expect(toast.type).toBe('info'); + expect(toast.action?.label).toBe('Undo'); + }); - expect(latest.length).toBe(1); + it('runs the undo handler and adds nothing else when the undo succeeded', () => { + const handler = vi.fn().mockReturnValue(true); + service.showUndo('File replaced', handler, 0); + + latestToasts()[0].action?.handler(); + + expect(handler).toHaveBeenCalledTimes(1); + expect(latestToasts().length).toBe(1); + }); + + it('explains why when the action can no longer be undone', () => { + service.showUndo('File replaced', () => false, 0); + + latestToasts()[0].action?.handler(); + + const toasts = latestToasts(); + expect(toasts.length).toBe(2); + expect(toasts[1].type).toBe('info'); + expect(toasts[1].message).toBe("Can't undo — other changes were made since."); + }); + }); + + it('should auto-remove toast after duration', () => { + vi.useFakeTimers(); + try { + service.show('Temp', 'info', 50); // 50ms duration + let latest: Toast[] = []; + service.toasts$.subscribe(t => (latest = t)); - setTimeout(() => { + expect(latest.length).toBe(1); + + vi.advanceTimersByTime(100); expect(latest.length).toBe(0); - done(); - }, 100); + } finally { + vi.useRealTimers(); + } + }); + + describe('action toasts and pause/resume', () => { + function latestToasts(): Toast[] { + let latest: Toast[] = []; + service.toasts$.subscribe(t => (latest = t)).unsubscribe(); + return latest; + } + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2026, 0, 1)); + }); + + afterEach(() => vi.useRealTimers()); + + it('keeps an undo toast for at least 10 s by default', () => { + service.showUndo('File replaced', () => true); + + expect(latestToasts()[0].duration).toBe(MIN_ACTION_TOAST_MS); + vi.advanceTimersByTime(MIN_ACTION_TOAST_MS - 1); + expect(latestToasts().length).toBe(1); + vi.advanceTimersByTime(1); + expect(latestToasts().length).toBe(0); + }); + + it('extends a too-short duration when the toast has an action', () => { + service.show('Saved', 'info', 2000, { label: 'Undo', handler: () => undefined }); + + expect(latestToasts()[0].duration).toBe(MIN_ACTION_TOAST_MS); + }); + + it('keeps a sticky (duration 0) action toast sticky', () => { + service.show('Saved', 'info', 0, { label: 'Undo', handler: () => undefined }); + + vi.advanceTimersByTime(60000); + expect(latestToasts().length).toBe(1); + }); + + it('does not auto-dismiss while paused and resumes with the remaining time', () => { + service.show('Temp', 'info', 1000); + const [toast] = latestToasts(); + + vi.advanceTimersByTime(400); + service.pause(toast.id); + vi.advanceTimersByTime(5000); + expect(latestToasts().length).toBe(1); + + service.resume(toast.id); + vi.advanceTimersByTime(599); + expect(latestToasts().length).toBe(1); + vi.advanceTimersByTime(1); + expect(latestToasts().length).toBe(0); + }); + + it('ignores pause/resume for unknown or already removed toasts', () => { + service.show('Temp', 'info', 1000); + const [toast] = latestToasts(); + service.remove(toast.id); + + expect(() => { + service.pause(toast.id); + service.resume(toast.id); + service.pause(999); + }).not.toThrow(); + vi.advanceTimersByTime(2000); + expect(latestToasts().length).toBe(0); + }); + + it('stops pending timers on clear()', () => { + service.show('A', 'info', 1000); + service.clear(); + service.show('B', 'info', 0); + + vi.advanceTimersByTime(2000); + expect(latestToasts().map(t => t.message)).toEqual(['B']); + }); }); }); diff --git a/src/app/services/toast.service.ts b/src/app/services/toast.service.ts index 66ef54e..bc1e553 100644 --- a/src/app/services/toast.service.ts +++ b/src/app/services/toast.service.ts @@ -11,7 +11,26 @@ export interface Toast { message: string; type: 'success' | 'error' | 'info' | 'warning'; duration?: number; - action?: ToastAction; // optional inline action button (e.g. "Rückgängig") + action?: ToastAction; // optional inline action button (e.g. "Undo") +} + +/** Shown when a toast's "Undo" can no longer revert the action it refers to. */ +export const UNDO_UNAVAILABLE_MESSAGE = "Can't undo — other changes were made since."; + +/** Default auto-dismiss delay of a plain toast. */ +export const DEFAULT_TOAST_MS = 5000; + +/** + * Minimum lifetime of a toast that offers an action (WCAG 2.2.1): the user + * needs time to notice, read and reach the button before it disappears. + */ +export const MIN_ACTION_TOAST_MS = 10000; + +/** Auto-dismiss bookkeeping of one toast; `handle` is null while paused. */ +interface DismissTimer { + handle: ReturnType | null; + remaining: number; + startedAt: number; } @Injectable({ @@ -21,33 +40,42 @@ export class ToastService { private toastsSubject = new BehaviorSubject([]); public toasts$ = this.toastsSubject.asObservable(); private nextId = 0; + private timers = new Map(); - show(message: string, type: Toast['type'] = 'info', duration = 5000, action?: ToastAction): void { + show(message: string, type: Toast['type'] = 'info', duration = DEFAULT_TOAST_MS, action?: ToastAction): void { + const lifetime = action && duration > 0 ? Math.max(duration, MIN_ACTION_TOAST_MS) : duration; const toast: Toast = { id: this.nextId++, message, type, - duration, + duration: lifetime, action, }; const currentToasts = this.toastsSubject.getValue(); this.toastsSubject.next([...currentToasts, toast]); - // Auto-remove after duration - if (duration > 0) { - setTimeout(() => { - this.remove(toast.id); - }, duration); + if (lifetime > 0) { + this.startTimer(toast.id, lifetime); } } /** - * Confirmation toast for a reversible action, with an inline "Rückgängig" button. - * Defaults to a 6s window before auto-dismiss. + * Confirmation toast for a reversible action, with an inline "Undo" button. + * `undoHandler` must revert exactly the action the toast refers to and return + * false when that is no longer possible (e.g. other changes were made since); + * the user then gets an explanatory info toast instead of a silent no-op. + * Stays at least MIN_ACTION_TOAST_MS before auto-dismiss. */ - showUndo(message: string, undoHandler: () => void, duration = 6000): void { - this.show(message, 'info', duration, { label: 'Rückgängig', handler: undoHandler }); + showUndo(message: string, undoHandler: () => boolean, duration = MIN_ACTION_TOAST_MS): void { + this.show(message, 'info', duration, { + label: 'Undo', + handler: () => { + if (!undoHandler()) { + this.info(UNDO_UNAVAILABLE_MESSAGE); + } + }, + }); } success(message: string, duration?: number): void { @@ -66,12 +94,43 @@ export class ToastService { this.show(message, 'warning', duration); } + /** Stops a toast's auto-dismiss countdown (pointer or focus is on it). */ + pause(id: number): void { + const timer = this.timers.get(id); + if (!timer || timer.handle === null) return; + clearTimeout(timer.handle); + const elapsed = Date.now() - timer.startedAt; + this.timers.set(id, { handle: null, remaining: Math.max(0, timer.remaining - elapsed), startedAt: 0 }); + } + + /** Continues a paused countdown with the time that was left. */ + resume(id: number): void { + const timer = this.timers.get(id); + if (!timer || timer.handle !== null) return; + this.startTimer(id, timer.remaining); + } + remove(id: number): void { + this.stopTimer(id); const currentToasts = this.toastsSubject.getValue(); this.toastsSubject.next(currentToasts.filter(toast => toast.id !== id)); } clear(): void { + [...this.timers.keys()].forEach(id => this.stopTimer(id)); this.toastsSubject.next([]); } + + private startTimer(id: number, delay: number): void { + const handle = setTimeout(() => this.remove(id), delay); + this.timers.set(id, { handle, remaining: delay, startedAt: Date.now() }); + } + + private stopTimer(id: number): void { + const handle = this.timers.get(id)?.handle; + if (handle !== undefined && handle !== null) { + clearTimeout(handle); + } + this.timers.delete(id); + } } diff --git a/src/app/shared/components/color-scale-selector/color-scale-selector.component.html b/src/app/shared/components/color-scale-selector/color-scale-selector.component.html index 7ab743f..83fb46b 100644 --- a/src/app/shared/components/color-scale-selector/color-scale-selector.component.html +++ b/src/app/shared/components/color-scale-selector/color-scale-selector.component.html @@ -1,42 +1,63 @@ -
    -
    +
    +
    + @if (dropdownOpen) { -
    +
    @for (group of groupedColorScales; track group.group; let gi = $index) { -
    {{ group.group }}
    - @for (scale of group.scales; track scale.id) { -
    - @if (scale.type === 'numeric') { -
    - } @else { - @for (c of getCategoricalColors(scale); track c) { -
    +
    + + @for (scale of group.scales; track scale.id) { +
    + {{ scale.name }} + @if (scale.type === 'numeric') { + + } @else { + @for (c of getCategoricalColors(scale); track c) { + + } } - } -
    - } +
    + } +
    @if (gi < groupedColorScales.length - 1) { -
    + } }
    diff --git a/src/app/shared/components/color-scale-selector/color-scale-selector.component.scss b/src/app/shared/components/color-scale-selector/color-scale-selector.component.scss index 5e5e595..b021bdd 100644 --- a/src/app/shared/components/color-scale-selector/color-scale-selector.component.scss +++ b/src/app/shared/components/color-scale-selector/color-scale-selector.component.scss @@ -2,4 +2,26 @@ .color-scale-select { @include m.color-scale-select; + + // The trigger is a real button; keep the field look from the mixin. + .selected-scale { + width: 100%; + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + + &:focus-visible { + @include m.focus-ring; + } + } + + .color-scale-option:focus-visible { + @include m.focus-ring(-2px); + } + + // Scale names for screen readers; sighted users see the color previews. + .visually-hidden { + @include m.visually-hidden; + } } diff --git a/src/app/shared/components/color-scale-selector/color-scale-selector.component.spec.ts b/src/app/shared/components/color-scale-selector/color-scale-selector.component.spec.ts new file mode 100644 index 0000000..91ad993 --- /dev/null +++ b/src/app/shared/components/color-scale-selector/color-scale-selector.component.spec.ts @@ -0,0 +1,142 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ColorScaleSelectorComponent } from './color-scale-selector.component'; +import { COLOR_SCALES, ColorScale, buildGroupedColorScales, getColorScaleById } from '../../interfaces/color-scale'; + +@Component({ + standalone: true, + imports: [ColorScaleSelectorComponent], + template: ` + Color scale + + + `, +}) +class HostComponent { + labelledby: string | null = 'host-scale-label'; + groups = buildGroupedColorScales(); + selected: ColorScale = getColorScaleById(0); + picked: number[] = []; + + pick(id: number): void { + this.picked = [...this.picked, id]; + this.selected = getColorScaleById(id); + } +} + +/** The picker must be nameable and fully keyboard-operable (WCAG 2.1.1, 4.1.2). */ +describe('ColorScaleSelectorComponent – accessibility', () => { + let fixture: ComponentFixture; + let host: HostComponent; + let el: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [HostComponent] }).compileComponents(); + fixture = TestBed.createComponent(HostComponent); + host = fixture.componentInstance; + el = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + const trigger = (): HTMLButtonElement => { + const button = el.querySelector('button.selected-scale'); + if (!button) throw new Error('trigger not rendered'); + return button; + }; + const listbox = (): HTMLElement | null => el.querySelector('[role="listbox"]'); + const options = (): HTMLElement[] => Array.from(el.querySelectorAll('[role="option"]')); + const focusedName = (): string => (document.activeElement?.textContent ?? '').trim(); + + function key(target: Element, name: string): void { + target.dispatchEvent(new KeyboardEvent('keydown', { key: name, bubbles: true, cancelable: true })); + fixture.detectChanges(); + } + + it('is named by the given label plus the current scale', () => { + const ids = (trigger().getAttribute('aria-labelledby') ?? '').split(' '); + + expect(ids[0]).toBe('host-scale-label'); + expect(document.getElementById(ids[1])?.textContent?.trim()).toBe(COLOR_SCALES[0].name); + expect(trigger().hasAttribute('aria-label')).toBe(false); + }); + + it('falls back to "Color scale: " without a label element', () => { + host.labelledby = null; + fixture.detectChanges(); + + expect(trigger().getAttribute('aria-label')).toBe(`Color scale: ${COLOR_SCALES[0].name}`); + expect(trigger().hasAttribute('aria-labelledby')).toBe(false); + }); + + it('exposes a collapsed listbox popup that expands on click', () => { + expect(trigger().getAttribute('aria-haspopup')).toBe('listbox'); + expect(trigger().getAttribute('aria-expanded')).toBe('false'); + expect(listbox()).toBeNull(); + + trigger().click(); + fixture.detectChanges(); + + expect(trigger().getAttribute('aria-expanded')).toBe('true'); + expect(listbox()?.id).toBe(trigger().getAttribute('aria-controls') ?? 'missing'); + }); + + it('groups named options and marks the selected one', () => { + trigger().click(); + fixture.detectChanges(); + + expect(el.querySelectorAll('[role="listbox"] > [role="group"]').length).toBe(host.groups.length); + expect(options().map(o => o.textContent?.trim())).toEqual(host.groups.flatMap(g => g.scales.map(s => s.name))); + const selected = options().filter(o => o.getAttribute('aria-selected') === 'true'); + expect(selected.map(o => o.textContent?.trim())).toEqual([COLOR_SCALES[0].name]); + }); + + it('opens with ArrowDown on the selected scale, moves with the arrow keys and picks with Enter', () => { + key(trigger(), 'ArrowDown'); + expect(focusedName()).toBe(COLOR_SCALES[0].name); + + key(document.activeElement as Element, 'ArrowDown'); + key(document.activeElement as Element, 'ArrowDown'); + expect(focusedName()).toBe(options()[2].textContent?.trim()); + + key(document.activeElement as Element, 'Enter'); + + expect(host.picked).toEqual([host.groups.flatMap(g => g.scales)[2].id]); + expect(listbox()).toBeNull(); + expect(document.activeElement).toBe(trigger()); + }); + + it('supports Home, End and Space', () => { + key(trigger(), 'ArrowDown'); + key(document.activeElement as Element, 'End'); + expect(focusedName()).toBe(options()[options().length - 1].textContent?.trim()); + + key(document.activeElement as Element, 'Home'); + expect(focusedName()).toBe(options()[0].textContent?.trim()); + + key(document.activeElement as Element, ' '); + expect(host.picked).toEqual([host.groups.flatMap(g => g.scales)[0].id]); + }); + + it('closes on Escape without picking and returns focus to the button', () => { + key(trigger(), 'ArrowDown'); + key(document.activeElement as Element, 'Escape'); + + expect(listbox()).toBeNull(); + expect(host.picked).toEqual([]); + expect(document.activeElement).toBe(trigger()); + }); + + it('closes when focus leaves the picker', () => { + key(trigger(), 'ArrowDown'); + + el.querySelector('#host-outside')?.focus(); + fixture.detectChanges(); + + expect(listbox()).toBeNull(); + }); +}); diff --git a/src/app/shared/components/color-scale-selector/color-scale-selector.component.ts b/src/app/shared/components/color-scale-selector/color-scale-selector.component.ts index 9c6071b..02514ef 100644 --- a/src/app/shared/components/color-scale-selector/color-scale-selector.component.ts +++ b/src/app/shared/components/color-scale-selector/color-scale-selector.component.ts @@ -1,6 +1,17 @@ -import { Component, Input, Output, EventEmitter, HostListener } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; import { ColorScale, getContinuousGradient, getCategoricalColors } from '../../interfaces/color-scale'; +let nextSelectorId = 0; + +/** + * Color scale picker: a button that opens a listbox of scale previews + * (select-only combobox pattern). Keyboard: Enter, Space, ArrowDown or ArrowUp + * open the list on the selected scale; ArrowUp/ArrowDown/Home/End move between + * scales; Enter or Space picks one; Escape closes; Tab leaves. + * + * A `