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 d6d5f7c..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 @@ -42,9 +45,13 @@ testem.log Thumbs.db # Miscellaneous -/.angular/cache -/src/assets/__pycache__ +/.angular/cache +__pycache__/ +.pytest_cache/ /src/assets/tests /.claude /.angular -Claude.md \ No newline at end of file +Claude.md + +# Planning artifacts (brainstorming/plans/specs) — keep out of the repo +/docs/superpowers/ \ No newline at end of file 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 new file mode 100644 index 0000000..fd93f60 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,424 @@ +# Implementierungs-Tickets – Wizard-Überarbeitung + +Ticketsystem für die Umsetzungsphase. Als Backbone dient der **Anforderungskatalog (A1–A15)** +Jede Anforderung ist ein Ticket. Feingranulare Einzelbefunde werden nicht als eigene Tickets geführt, sondern innerhalb des jeweiligen Anforderungstickets als abzuhakende Punkte gelistet. + +Pfadangaben sind relativ zu `Code/src/app/`. Der Wizard liegt vollständig unter `preprocessing-wizard/`. + +Miro-Board (Lösungskonzepte): https://miro.com/app/board/uXjVGrxvTQs=/ + +**Status-Legende:** `TODO` · `In Progress` · `In Feature-Branch` (fertig entwickelt, in `feature/wizard-redesign` gemergt, noch nicht in `main`) · `Finished` (in `main`) +**Typ:** `Konzept` (durchgeplant in Miro) · `trivial` (kleiner, klar umrissener Fix) + +### Branch-Strategie + +Integrations-Branch `feature/wizard-redesign` von `main` abzweigen. Pro Ticket ein kurzer Branch davon, dort das Feature bauen, dann per Merge/PR zurück in `feature/wizard-redesign`. Am Ende der ganze Feature-Branch nach `main`. + +- Ticket-Branch-Namensschema: `feat/aN-kurzbezeichnung` (z. B. `feat/a4-undo-historie`). +- Ticket-Branches klein halten und zügig zurückmergen, sonst driftet der Feature-Branch weg. +- Regelmäßig `main` in `feature/wizard-redesign` mergen, damit der finale Merge klein bleibt. +- Statusübergang: `TODO` → (Branch anlegen) `In Progress` → (in Feature-Branch gemergt) `In Feature-Branch` → (Feature-Branch in `main`) `Finished`. + + +--- + +## Übersicht + +| Ticket | Kurztitel | Typ | Status | Branch | Hauptort im Code | +|--------|-----------|-----|--------|--------|------------------| +| A1 | Fachbegriffe/Optionen erklären | Konzept | In Feature-Branch | `feat/a1-a11-erklaerungen-defekte` | `shared/constants/help-text.ts`, `shared/help-tooltip/`, `shared/projection-preview/`, Step 3+4 | +| A2 | Voreinstellungen sichtbar/umkehrbar | Konzept | In Feature-Branch | `feat/a2-smart-defaults` | Step 2+3, `services/preprocessing.service.ts` | +| A3 | Fehlermeldungen inline/verständlich | Konzept | In Feature-Branch | `feat/a3-fehleranzeige` | Step 4+5 | +| A4 | Undo/Zurücksetzen | Konzept | In Feature-Branch | `feat/a4-undo-historie` | `services/preprocessing.service.ts`, Step 1+4 | +| A5 | Zusammengehöriges gruppieren | Konzept | In Feature-Branch | `feat/a5-gruppierung` | Step 3+4 | +| A6 | Review-Navigation (Direktsprünge) | Konzept | In Feature-Branch | `feat/a6-direct-links` | Step 5, `shared/progress-stepper/` | +| A7 | Ehrliche/konsistente Signifier | trivial | In Feature-Branch | `feat/a10-a7-polish` | Step 1/2/4, `shared/progress-stepper/` | +| A8 | Systemstatus/Datenanzeige korrekt | trivial | In Feature-Branch | `feat/a8-datenanzeige` | Step 2+4 | +| A9 | Beschleuniger für Power-Nutzer | Konzept | In Feature-Branch | `feat/a9-power-shortcuts` | Step 2/3/4 | +| A10 | Layout gegen lange/viele Inhalte | Konzept | In Feature-Branch | `feat/a10-a7-polish` | Step 1+4, `shared/_wizard-shared.scss` | +| A11 | Technische Defekte beheben | trivial | In Feature-Branch | `feat/a1-a11-erklaerungen-defekte` | siehe Ticket | +| A12 | Zweispaltiges Layout | Konzept | In Feature-Branch | `feat/a12-two-column` | `preprocessing-wizard.component.*`, `shared/progress-stepper/` | +| A13 | Schrittlogik klären/neu ordnen | Konzept | In Feature-Branch | `feat/a13-schrittlogik` | Step 2/3/4, `shared/constants/step-info.ts` | +| A14 | Übergänge sichtbar (Animation) | Konzept | In Feature-Branch | `feat/a14-animation` | Step 2↔3, `shared/data-preview-table/` | +| A15 | Einheitliches Tabellen/Listen-System | Konzept | In Feature-Branch | `feat/a15-table-system` | `shared/data-preview-table/`, Step 2/3/4 | + +**Aktueller Stand (Integrations-Branch `feature/wizard-redesign`):** +- Gemergt (In Feature-Branch): A1–A15 vollständig — alle Tickets sind im Integrations-Branch. +- In Arbeit: keine. +- Offen (TODO): keine (A2-Nachtrag umgesetzt und gemergt, PR #85). +- Nächster Schritt: `feature/wizard-redesign` nach `main` mergen; danach alle Tickets auf `Finished` setzen. +- Aufräumung: Planungsartefakte (`docs/superpowers/`) entfernt und via `.gitignore` künftig ausgeschlossen (PR #84). +- Gebündelt umgesetzt: A7+A10 in `feat/a10-a7-polish`; A1+A11 in `feat/a1-a11-erklaerungen-defekte`. +- Doku: Jedes Ticket hat einen „Umgesetzt"-Abschnitt mit PR-Verweis. Die Abschnitte für A2–A10 und A12–A15 wurden nach dem Merge aus Code und PR-Beschreibungen abgeleitet (A1/A11 direkt aus der Umsetzung). + +--- + +## Umsetzungsreihenfolge und Abhängigkeiten + +Die Reihenfolge ist von den Abhängigkeiten getrieben, nicht von der Katalognummer. Grundgedanke: erst das strukturelle Fundament (Layout-Shell, Schrittlogik, gemeinsame Tabelle), dann die Features, die darauf aufsetzen, zuletzt der Feinschliff. Undo (A4) kommt spät, weil es zum einen den Platz im großen Layout braucht und zum anderen den Zustand sichern muss, den erst die Smart Defaults (A2) festlegen. + +| Phase | Reihenfolge | Ticket | Blockiert von | Begründung | +|-------|-------------|--------|---------------|------------| +| 1 – Fundament | 1 | A12 Zweispaltiges Layout | — | Die Shell, in der alle Schritte rendern. Muss zuerst stehen. | +| 1 – Fundament | 2 | A13 Schrittlogik/Neuzuschnitt Schritt 4 | A12 | Legt fest, welcher Inhalt in welchem Schritt lebt; spätere Tickets bauen darauf. | +| 1 – Fundament | 3 | A15 Tabellen/Listen-System | A12 (inhaltlich A13) | Gemeinsame Tabellenkomponente, die A2, A8, A9, A14 wiederverwenden. | +| 2 – Features | 4 | A5 Gruppierung/Parameter in Box | A13 | Setzt den Neuzuschnitt von Schritt 4 voraus. | +| 2 – Features | 5 | A2 Smart Defaults sichtbar/umkehrbar | A15 | Braucht die Tabelle, um Auswahl-/Default-Zustand anzuzeigen. | +| 2 – Features | 6 | A14 Übergangsanimation 2↔3 | A15, A13 | Tabelle ist Träger des Morphings; Schrittstruktur muss stehen. | +| 2 – Features | 7 | A9 Power-Beschleuniger | A15 | Shift-Klick, Select-all-gefiltert, Drag-and-drop bauen auf der Tabelle/Liste auf. | +| 2 – Features | 8 | A10 Layout-Robustheit/Feature-Suche | A15, A12 | Durchsuchbare Feature-Liste und Umbruch im neuen Layout. | +| 2 – Features | 9 | A6 Review-Direktlinks | A13 | Sprünge zielen auf die final zugeschnittenen Schritte. | +| 2 – Features | 10 | A8 Datenanzeige („-"→„0", Zeilenkontext) | A15 | Anzeige in der gemeinsamen Tabelle. | +| 3 – Zustand/Inhalt | 11 | A3 Fehleranzeige/Persistent Result | A13 | Step 4/5 müssen strukturell stehen. | +| 3 – Zustand/Inhalt | 12 | A4 Undo/Historie | A12, A2 | Braucht Platz im Layout und ein gesetztes State-/Reset-Modell. | +| 4 – Feinschliff | 13 | A1 Erklärtexte/Tooltips | — (locker) | Inhaltsschicht; sinnvoll, wenn die Komponenten existieren. | +| 4 – Feinschliff | 14 | A7 Signifier | — | Kosmetisch; final nach dem Restyle durch A12/A15. | +| 4 – Feinschliff | 15 | A11 Technische Defekte | teils — | Flaky Tooltip und doppelte „Moderate"-Option sind unabhängige Quick Wins (jederzeit, gern zuerst für Momentum); Persistent-Result-Teil zusammen mit A3. | + +```mermaid +graph TD + A12[A12 Zweispalten-Layout] --> A13[A13 Schrittlogik] + A12 --> A15[A15 Tabellen-System] + A13 --> A15 + A13 --> A5[A5 Gruppierung/Parameter] + A15 --> A2[A2 Smart Defaults] + A15 --> A14[A14 Animation 2 zu 3] + A13 --> A14 + A15 --> A9[A9 Power-Beschleuniger] + A15 --> A10[A10 Layout-Robustheit] + A12 --> A10 + A13 --> A6[A6 Review-Direktlinks] + A15 --> A8[A8 Datenanzeige] + A13 --> A3[A3 Fehleranzeige] + A12 --> A4[A4 Undo/Historie] + A2 --> A4 + A1[A1 Erklaertexte] + A7[A7 Signifier] + A11[A11 Technische Defekte / Quick Wins] +``` + +Quick Wins ohne Abhängigkeit, die sich parallel zum Fundament wegräumen lassen: A11 (flaky Tooltip, doppelte Option), A7 (Signifier), teils A1 (Erklärtexte). + +--- + +## A1 – Fachbegriffe und Optionen verständlich erklären +**Typ:** Konzept · **Miro:** Visual Tooltips Methoden · **Status:** In Feature-Branch (PR #83 gemergt, gebündelt mit A11) · **Branch:** `feat/a1-a11-erklaerungen-defekte` +**Befunde:** C-S4-01, C-S4-03, C-S4-08, C-S4-10, S-S3-02, S-S3-04, P-S3-04 + +**Problem:** Fachbegriffe werden ohne Erklärung angeboten (Farbattribut, Projektionsmethoden, FastMap, Ausreißermethoden, Z-Wert-Standardisierung, Smart Defaults). + +**Hinweis:** Aus A5 übernommen: Die unerklärten Farbabweichungen bzw. die „Farbauswahl" sind hier über einen erklärenden Text zu lösen, nicht über ein zusätzliches Farb-Steuerelement. + +**Wo umsetzen:** +- Erklärtexte zentral pflegen in `shared/constants/help-text.ts`. +- Abrufbare Erklärung (Progressive Disclosure) über die bestehende `shared/help-tooltip/`-Komponente; für Projektionsmethoden auf visuelle Tooltips erweitern (Miro „Visual Tooltips Methoden", vgl. `tmp/Projektion_Methoden_Visualisierung.html`). +- Einbindung an den Optionen selbst: Farbattribut und Projektionen in `steps/step4-visualization-settings/`, Ausreißer-/Standardisierungsoptionen in `steps/step3-configure-data-features/`. + +**Umgesetzt (PR #83):** +- Neue Komponente `shared/projection-preview/`: animierte Canvas-Vorschau je Projektionsmethode (aus ClaudeDesign-Vorlage „Projection Previews" portiert; baut/animiert nur die angefragte Methode, läuft nur bei geöffnetem Popup, Tempo 0.5). Eingebunden als Hilfe-Icon mit Vorschau-Popup an jeder Projektionskarte in Step 4 und bei FastMap (Badge „Always active"). +- Erklärtexte für Farbattribut (`colorFeature`) und Farbskala (`colorScale`) in Step 4 verdrahtet; Farbskala-Text weist auf den rein kosmetischen Charakter hin. +- Smart-Defaults-Erklärtext (`smartDefaults`) an der Spalten-Konfigurationskarte in Step 3. +- Ausreißer-Prinzipien (IQR vs. Z-Score) und der Strenge-Hinweis kompakt in den Outlier-Tooltip aufgenommen (erklärt zugleich die wiederkehrenden Strenge-Labels, Bezug zu C-S3-04). +- Standardisierung/Z-Wert und Ausreißermethoden waren bereits über bestehende `tableHeaders`-Tooltips erklärt. +- Nicht umgesetzt: keine statische Bild-Grafik nötig — die Erklärung erfolgt über die animierte Live-Vorschau statt über `tmp/Projektion_Methoden_Visualisierung.html`. + +--- + +## A2 – Voreinstellungen sichtbar und umkehrbar machen +**Typ:** Konzept · **Miro:** Smart Defaults Button · **Status:** In Feature-Branch · **Branch:** `feat/a2-a9-defaults-power` +**Befunde:** C-S2-01, C-S3-02, S-S3-01, P-S3-04 + +**Problem:** Smart Defaults sind unsichtbar; unklar was automatisch gesetzt wurde; geänderte Defaults sind nicht reaktivierbar; in der Spaltenauswahl fehlt eine sinnvolle Vorauswahl. + +**Wo umsetzen:** +- Smart-Default-Logik/State in `services/preprocessing.service.ts` (Kennzeichnung „vom Default abweichend", Reset-Funktion). +- Vorauswahl beim Spalten-Import in `steps/step2-column-selection/` (offensichtlich ungeeignete Spalten vorab abwählen; vgl. `missingPercentage > 50 || uniqueCount === 1`). +- Vorauswahl erweitern um Hochkardinalität: Spalten mit (nahezu) 100 % Uniqueness (`uniqueCount === totalRows` bzw. `uniqueCount / totalRows` nahe 1 — typisch IDs, Titel, Namen, Freitext) beim Import automatisch abwählen, sichtbar gekennzeichnet und wie jeder Default umkehrbar. Ergänzt die bestehende Heuristik um den Fall, der bisher erst zur Laufzeit auffällt. +- Sichtbare Kennzeichnung + „Smart Defaults zurücksetzen/erneut anwenden"-Button in `steps/step3-configure-data-features/` und `steps/step4-visualization-settings/`. + +**Bezug (aus A3 / Fehlerklasse K7):** In A3 wurde der *reaktive* Teil dieses Problems abgedeckt — der Prozessor bricht bei einer One-Hot-Explosion früh und verständlich ab (`src/assets/preprocessing_processor_config.py`, Schwelle `MAX_ONEHOT_UNIQUE`), und die Fehlerklasse K7 (`shared/constants/wizard-error-classes.ts`) benennt die betroffenen Spalten und schlägt Abwählen bzw. Label-Encoding vor. Konkreter Auslöser war `streaming_titles.csv` mit fünf pro Zeile einzigartigen Spalten (`show_id`, `title`, `director`, `cast`, `description`): One-Hot hätte daraus ~43.750 Spalten erzeugt und den (Pyodide-)Speicher gesprengt. A2 soll denselben Fall *proaktiv* entschärfen — solche Spalten gar nicht erst standardmäßig aktiv lassen, statt den Nutzer erst im Fehlerfall (K7) gegensteuern zu lassen. + +**Umgesetzt (PR #78, `feat/a2-a9-defaults-power`):** +- Smart Defaults werden im Service aus `DATA_TYPE_CONFIG` abgeleitet (`getColumnDefaults`, `createDefaultColumnConfig` in `services/preprocessing.service.ts`); das alte „Smart defaults applied"-Banner in Schritt 3 wurde entfernt. +- Abweichungserkennung je Einstellung in `steps/step3-configure-data-features/` (`isEncodingModified`, `isScalingModified`, `isMissingModified`, `isOutlierModified`, Sammel-Flag `isColumnModified`) mit sichtbarer Kennzeichnung. +- Umkehrbarkeit: Reset je Einstellung (`resetEncoding/Scaling/Missing/Outliers`) plus „Reset to defaults" für die ganze Spalte (`resetColumnToDefault`). +- Proaktive Vorauswahl beim Import: `createDefaultColumnConfig` setzt `enabled: !hasIssues` mit `hasIssues = missingPercentage > 50 || uniqueCount === 1`; solche Spalten starten abgewählt und sind in Schritt 2 reaktivierbar (entschärft One-Hot-Explosion proaktiv, Bezug K7). +- Nicht umgesetzt: die zusätzlich geplante Hochkardinalitäts-Heuristik (`uniqueCount ≈ totalRows`, typische IDs/Freitext) fehlt in `hasIssues` — vorab abgewählt werden nur konstante bzw. überwiegend leere Spalten. + +**Nachtrag (In Feature-Branch, PR #85 gemergt) — Smart Defaults erweitern · Branch:** `feat/a2-nachtrag-smart-defaults` +Eigenständiges Folge-Arbeitspaket zu A2, umgesetzt und gemergt (7 neue Unit-Tests für die Auto-Deselect-Heuristik). Umfang „Hoch + Mittel": +- *Hoch — Hochkardinalität in Schritt 2:* Spalten mit nahezu 100 % einzigartigen Werten proaktiv abwählen. Heuristik: `uniqueCount / count >= 0.9` (ggf. 0.95) **und** `count > ~20`, beschränkt auf Text/Categorical/ID (numerische Messwerte ausnehmen). Nutzt vorhandene Felder (`column-statistics.ts`, `getUniquePercent`), kein neues Datenmodell. Umsetzung in `createDefaultColumnConfig` (`services/preprocessing.service.ts:283`) + gespiegelt in `steps/step2-column-selection/` (ts:200) — mit eigenem Grund (`issueDescription`) statt Vermischung in `hasIssues`. +- *Hoch — sichtbare Kennzeichnung:* expliziter „automatisch abgewählt"-Marker in Schritt 2 (getrennt vom Qualitäts-Warnicon) mit Kurzbegründung; Tipp erweitern um den Fall 100 % einzigartiger Werte (IDs/Titel/Freitext) und den Hinweis auf Reaktivierbarkeit. +- *Mittel — klügerer `colorFeature`-Default:* statt willkürlich `columns[0]` (`step4…ts:387-395`) ein niedrig-kardinales Kategoriemerkmal bzw. eine varianzstarke Numeric wählen und als Default sichtbar kennzeichnen. +- *Bewusst außen vor (niedrig):* typabhängige Missing-Imputation (Median/Mean) und aggressivere Ausreißer-Defaults — konservatives `Keep` bleibt. + +--- + +## A3 – Fehlermeldungen inline, verständlich und lösungsorientiert +**Typ:** Konzept · **Miro:** Fehleranzeige, NEU: Persistent Result Screen + Errorhandling · **Status:** In Feature-Branch · **Branch:** `feat/a3-fehleranzeige` +**Befunde:** C-S4-05, C-S5-02, P-S5-01 + +**Problem:** Fehler erscheinen spät, entfernt vom Ort, inhaltlich nichtssagend, teils nur als flüchtiges Popup. + +**Wo umsetzen:** +- Feldnahe Inline-Validierung in `steps/step4-visualization-settings/` (Fehler am jeweiligen Parameter). +- Verständliche, dauerhaft sichtbare Fehlermeldung im Review/Processing: `steps/step5-review-processing/` (Ursache + Lösung, kein verschwindendes Popup). +- Analysegrundlage: `fehlerbehandlung-wizard-analyse.html`, `fehlerklassen-ableitbarkeit.html`. + +**Umgesetzt (PR #81, `feat/a3-fehleranzeige`):** +- Neues Fehlerklassen-Modell in `shared/constants/wizard-error-classes.ts` (mit Spec): klassifiziert Worker-Fehler in verständliche, lösungsorientierte Meldungen (u. a. K4, K7). +- Ursachen-Fix in `services/data-processor.ts`: Worker-Fehler werden als echte `Error` (statt String) weitergereicht, damit die Ursache erhalten bleibt; `clearError()` gegen den beim Wiedereinstieg „klebenden" Fehler. +- Persistent Result Screen in `steps/step5-review-processing/` mit fünf Zuständen (Review/Pre-flight, Processing, blockierender Fehler, Partial, Success), „Your work is saved"-Pill und Methoden-Statusleiste; `ngOnInit` stellt Ergebnis/Fehler nach Wiedereinstieg wieder her (deckt P-S5-02/03 mit ab). +- „Fix in Schritt N"-Sprung aus dem Fehlerscreen (über die A6-Sprunglogik) plus Toast. +- Feldnahe Inline-Validierung in `steps/step4-visualization-settings/` (Meldung direkt unter den betroffenen Parametern bei K4; Glyph-Panel flaggt „zu viele" > 12). + +--- + +## A4 – Rückgängigmachen und Zurücksetzen anbieten +**Typ:** Konzept · **Miro:** Historie einführen · **Status:** In Feature-Branch (PR #82 gemergt) · **Branch:** `feat/a4-undo-historie` +**Befunde:** C-S1-02, C-S4-09, P-S3-01 +**Tests:** Automatisierte Tests für das Undo/Redo-Feature ergänzt (Testsuite grün). + +**Problem:** Folgenschwere Aktionen (erneuter Upload, Smart-Defaults-Klick, gesetzte Filter) sind nicht umkehrbar. Voraussetzung ist eine Aktionshistorie — im Code bisher **nicht vorhanden** (kein `undo`/`history` gefunden). + +**Wo umsetzen:** +- Historie/Undo-Stack neu im State: `services/preprocessing.service.ts` (Snapshot der `preprocessing-state`). +- Undo nach „Upload Different File": `steps/step1-upload/`. +- Undo für Smart-Defaults-Reset: `steps/step4-visualization-settings/`. +- Filter leeren: `steps/step3-configure-data-features/`. + +--- + +## A5 – Zusammengehöriges räumlich gruppieren +**Typ:** Konzept · **Miro:** Parameter Location, Aufbereitung Rework · **Status:** In Feature-Branch · **Branch:** `feat/a5-gruppierung` +**Erweiterter Umfang:** die Methodenbox klappt in-place auf (Höhe animiert, Breite konstant) und ist im nicht-aufgeklappten Zustand vollständig geschlossen; Reset je Box. Der Schritt-3-Tabellenteil von A5 wurde bewusst nicht hier, sondern im Zuge von A15 (Schritt-3-Redesign) behandelt. +**Befunde:** C-S3-01, C-S4-07, S-S4-03 + +**Problem:** Projektionsparameter liegen außerhalb ihres Methodenkastens (willkürlich platziert); Datenkonfiguration überladen, Spaltenzuordnung nicht erkennbar. *(Vom Nutzer als teils trivial markiert: Parameter in die zugehörige Parameterbox verschieben.)* + +**Wo umsetzen:** +- Parameter in den Kasten der jeweiligen Methode ziehen + Reset auf Defaultwerte: `steps/step4-visualization-settings/step4-visualization-settings.component.html` und `.ts` (Miro „Parameter Location"). +- Datenkonfiguration klarer strukturieren (Angabe↔Spalte erkennbar): `steps/step3-configure-data-features/`. + +**Hinweis:** Der Befund zu unerklärten Farbabweichungen bzw. zur „Farbauswahl" passt inhaltlich nicht zu A5 und wird nach A1 verschoben; dort ist er über einen erklärenden Text statt über ein Farb-Steuerelement zu lösen. + +**Umgesetzt (PR #74, `feat/a5-gruppierung`):** +- Projektionsparameter liegen jetzt in der jeweiligen Methodenbox in `steps/step4-visualization-settings/` (Aufklapp-Button `btn-expand-params` → `toggleMethodParams`, Zustand `expandedMethodParams`/`isMethodParamsExpanded`). +- In-place-Aufklappen: animierte Höhe bei konstanter Breite über `.params-wrapper`/`.params-clip` (Kollaps auf `0fr`); im geschlossenen Zustand vollständig zu. +- Reset je Box: `resetMethodParams(method)`, Button nur aktiv bei Abweichung (`methodParamsChanged`). +- Der Schritt-3-Tabellenteil wurde wie im Hinweis vermerkt nicht hier, sondern über A15 behandelt. + +--- + +## A6 – Navigation und Bearbeitung im Review-Schritt +**Typ:** Konzept · **Miro:** Direct Links · **Status:** In Feature-Branch · **Branch:** `feat/a6-a8-review-datenanzeige` +**Befunde:** C-S5-01, S-S5-01 + +**Problem:** Im Review lässt sich nichts direkt bearbeiten; Nutzer müssen sich merken, in welchem Schritt eine Einstellung lag. + +**Wo umsetzen:** +- Direktsprünge je Einstellungsblock in `steps/step5-review-processing/step5-review-processing.component.html`. +- Sprung-/Aktivierungslogik in `preprocessing-wizard.component.ts` bzw. `shared/progress-stepper/` (gezielt zu Schritt X wechseln). + +**Umgesetzt (PR #79, `feat/a6-a8-review-datenanzeige`):** +- Pro Summary-Card in `steps/step5-review-processing/step5-review-processing.component.html` ein Edit-Icon-Button → `editSetting(step, anchorId)` → `goToStepWithScroll(...)`. +- Scroll-to-Target über `scrollTargetSubject` (BehaviorSubject) in `preprocessing-wizard.component.ts`/`services/preprocessing.service.ts`; der Zielschritt scrollt per `scrollIntoView` zu unsichtbaren `scroll-anchor`-Ankern (additiv, keine URL-Deep-Links). +- Mapping: Columns → Schritt 2; Projection Features / Color / Glyph / Methods → Schritt 4 (jeweiliges Panel). + +--- + +## A7 – Ehrliche und konsistente Signifier +**Typ:** trivial · **Miro:** Änderungen · **Status:** In Feature-Branch · **Branch:** `feat/a10-a7-polish` (gebündelt mit A10) +**Befunde:** C-S1-01, C-S4-02, C-S3-03, S-S2-01 + +**Problem:** Aktive Navigationsleiste wirkt deaktiviert; Auswahlfeld sieht aus wie Eingabefeld; farbliche Abweichungen unerklärt; „Select All" suggeriert falschen Zustand. + +**Wo umsetzen:** +- Navigationsleisten-Farbschema: `shared/progress-stepper/progress-stepper.component.scss`. +- Auswahlfeld-Optik (Select statt Eingabe): `steps/step4-visualization-settings/`. +- „Select All"-Zustand: `steps/step2-column-selection/`. + +**Umgesetzt (PR #80, `feat/a10-a7-polish`, gebündelt mit A10):** +- Progress-Stepper: erreichbare/besuchte Schritte erscheinen in Akzent-Cyan statt ausgegraut, nur echte Zukunftsschritte bleiben grau (`shared/progress-stepper/progress-stepper.component.scss` + `.ts`). +- Step 4: COLOR-ATTRIBUTE-Feld als Select mit Chevron gestaltet (nicht mehr wie ein Texteingabefeld), einheitliche Chevrons. +- „Select All" (Step 2): Tri-State (indeterminate/checked) war bereits durch A9 korrekt — verifiziert, keine Änderung nötig. +- C-S3-03 (unerklärte Farbabweichungen) bewusst ausgelassen; laut ROADMAP nach A1 verschoben und dort per Erklärtext behandelt. + +--- + +## A8 – Systemstatus und Datenanzeige korrekt sichtbar machen +**Typ:** trivial · **Miro:** — · **Status:** In Feature-Branch · **Branch:** `feat/a6-a8-review-datenanzeige` +**Befunde:** C-S4-06, C-S2-03 + +**Problem:** Zeilenanzahl ohne Gesamtkontext; fehlende Werte als „-" statt „0". + +**Wo umsetzen:** +- „-"→„0" bei fehlenden Werten: `steps/step2-column-selection/` bzw. `shared/data-preview-table/` (Missing-Spalte). +- Zeilenanzahl im Gesamtkontext anzeigen: `steps/step4-visualization-settings/`. + +**Umgesetzt (PR #79, `feat/a6-a8-review-datenanzeige`, gebündelt mit A6):** +- Missing-Count „—" → „0" gezielt nur in der Statistikspalte von `steps/step2-column-selection/`; Rohdatenzellen (`shared/data-preview-table/`) und Distribution-„—" bewusst unverändert. +- Zeilenanzahl im Gesamtkontext: „N rows in dataset" (mit Tausendertrennung) im Projektionspanel von `steps/step4-visualization-settings/` (über das bestehende `getDatasetRowCount()`). + +--- + +## A9 – Beschleuniger für Power-Nutzer +**Typ:** Konzept · **Miro:** Glyph Feature Search Filter · **Status:** In Feature-Branch · **Branch:** `feat/a2-a9-defaults-power` +**Befunde:** P-S2-02, P-S3-02, P-S3-03, P-S4-01 + +**Problem:** Fehlende Tastenkürzel (Shift-Klick, Strg+F), keine gemeinsame Bearbeitung gleichartiger Spalten, kein „alle auswählen" bei aktivem Filter, keine Drag-and-drop-Reihenfolge der Features. + +**Wo umsetzen:** +- Shift-Klick/Mehrfachauswahl + „alle gefilterten auswählen": `steps/step2-column-selection/`, `steps/step3-configure-data-features/`. +- Bulk-Umstellung gleichartiger Spalten: `steps/step3-configure-data-features/`. +- Drag-and-drop-Reihenfolge der Features: `steps/step4-visualization-settings/`. +- Such-/Filterfeld als Navigationshilfe (Miro „Glyph Feature Search Filter"). + +**Umgesetzt (PR #78, `feat/a2-a9-defaults-power`):** +- Shift-Klick-Range-Select in `steps/step2-column-selection/` (`pendingShift`/`lastCheckedIndex`, über die gefilterte Liste). +- „Alle gefilterten auswählen": Select-All und Tri-State beziehen sich auf `filteredColumns`. +- Bulk-Umstellung gleichartiger Spalten in `steps/step3-configure-data-features/` („Apply to all N columns" mit Vorschau-Liste und Bestätigung). +- Drag-and-drop-Reihenfolge der Features in `steps/step4-visualization-settings/` (native DnD, `drag_indicator`-Handle; als eine Undo-Aktion). +- Such-/Filterfeld in Schritt 2 und 3 plus Tastenkürzel `/`, das das Suchfeld fokussiert (ignoriert Eingaben in Feldern). + +--- + +## A10 – Layout gegen lange Inhalte und viele Elemente absichern +**Typ:** Konzept · **Miro:** Glyph Feature Search Filter · **Status:** In Feature-Branch · **Branch:** `feat/a10-a7-polish` (gebündelt mit A7) +**Befunde:** P-S1-01, C-S4-11 + +**Problem:** Lange Namen brechen aus dem Layout; bei vielen Eigenschaften wird die Feature-Navigation unübersichtlich. + +**Wo umsetzen:** +- Umbruch/Kürzung mit Tooltip für langen Text: `shared/_wizard-shared.scss`, `steps/step1-upload/`. +- Platzsparende, durchsuchbare Feature-Liste: `steps/step4-visualization-settings/` (gemeinsam mit A9-Suchfeld). + +**Umgesetzt (PR #80, `feat/a10-a7-polish`, gebündelt mit A7):** +- Step 1: Dateiname in der Stat-Card kürzt per Ellipsis und zeigt den vollen Namen als `[title]`-Tooltip, bricht nicht mehr aus dem Grid (`steps/step1-upload/`, Truncation-Mixin in `src/styles/_mixins.scss`). +- `[title]`-Tooltips an Header- und Zellnamen der Data-Preview-Tabelle (`shared/data-preview-table/`) sowie an Feature-/Projektions-Spaltennamen in Step 4. +- Die platzsparende, durchsuchbare Feature-Liste (Kern von P-S1-01/C-S4-11) wurde bereits über A15 umgesetzt; der A10-Restumfang war entsprechend klein. + +--- + +## A11 – Technische Defekte beheben +**Typ:** trivial · **Miro:** Visual Tooltips Methoden, NEU: Persistent Result Screen + Errorhandling · **Status:** In Feature-Branch (PR #83 gemergt, gebündelt mit A1) · **Branch:** `feat/a1-a11-erklaerungen-defekte` +**Befunde:** C-S3-04, C-S4-04, S-S4-02, P-S5-02, P-S5-03 + +**Problem/Wo je Defekt:** +- **C-S4-04** Tooltip verschwindet beim Inspizieren (Flaky) → `shared/help-tooltip/help-tooltip.component.ts` (Hover-/Z-Index-Verhalten). *Vom Nutzer als trivial markiert.* +- **S-S3-03** verwandt (Tooltip auf falscher Z-Ebene) → gleiche Komponente / `steps/step3-configure-data-features/`. +- **C-S3-04** Option „Moderate" doppelt → `steps/step3-configure-data-features/` (Optionsliste). +- **S-S4-02** Schritt-Navigation springt nicht zurück zu Schritt 4 → `shared/progress-stepper/` bzw. `preprocessing-wizard.component.ts`. +- **P-S5-02 / P-S5-03** Zusammenfassung verschwindet beim Schließen während der Projektion / nichtssagender Screen nach Wiedereinstieg → `steps/step5-review-processing/` (Miro „Persistent Result Screen + Errorhandling"). → *Bereits im Zuge von A3 (Persistent Result Screen) abgedeckt und gemergt.* + +**Umgesetzt (PR #83):** +- **C-S4-04** Grace-Period im `shared/help-tooltip/` (und im neuen Projektions-Popup): Der Tooltip bleibt beim Hineinfahren offen (~180 ms Verzögerung), damit Inhalt/Vorschau überhaupt lesbar sind. +- **C-S3-04** Statt bloßem Umbenennen eine Erklärung der zwei Prinzipien (IQR vs. Z-Score) im Outlier-Tooltip; damit werden die wiederkehrenden Strenge-Labels („Moderate"/„Relaxed") verständlich. +- Fokus-Zustand korrigiert: Hilfe-/Vorschau-Icon bleibt nach Klick nicht dauerhaft blau (Farbe nur bei Hover / `:focus-visible`). +- Tooltip-Platzierung in Step 3 gefixt: `gsap.from()` (A14-Morph) hinterließ ein inline `transform` auf `.detail-well`, wodurch `position:fixed`-Tooltips relativ zur Welle statt zum Viewport rechneten; `clearProps: 'transform'` entfernt es nach der Animation. +- Keine horizontale Scrollbar mehr: Tooltips bis zur Positionsberechnung außerhalb des Sichtfelds geparkt, Clamping an `clientWidth/Height`; `.detail-well` `overflow-x: hidden`; `.detail-card` wächst mit dem Inhalt (kein Abschneiden). +- Bereits vorher behoben/abgedeckt (verifiziert, keine Änderung nötig): **S-S3-03** (Tooltip-Z-Ebene, `position:fixed` + hoher z-index), **S-S4-02** (Rücksprung zu Step 4 über den Stepper), **P-S5-02/03** (über A3). + +--- + +## A12 – Anzeigeraum effizient und zweispaltig nutzen +**Typ:** Konzept · **Miro:** Neue two column UI, Neuer Schritt 4 Anpassung · **Status:** In Feature-Branch · **Branch:** `feat/a12-two-column` +**Befund:** K-01 + +**Problem:** Vertikaler Bildschirmraum schlecht genutzt; Navigation über volle Breite erzeugt langes Scrollen. + +**Wo umsetzen:** +- Grundlayout (Navigation links, Arbeitsbereich rechts): `preprocessing-wizard.component.html` + `.scss`. +- Schritt-Navigation an den linken Rand: `shared/progress-stepper/`. + +**Umgesetzt (PR #72, `feat/a12-two-column`):** +- Zweispaltiges Grundlayout in `preprocessing-wizard.component.html`/`.scss`: linke `wizard-sidebar` mit `app-progress-stepper` (Schritt-Navigation am linken Rand), rechts der Arbeitsbereich. +- Navigationsleiste `.wizard-nav` in den rechten Arbeitsbereich integriert (nicht mehr über die volle Breite). +- Redundante Schritt-Header aus den Steps entfernt; die Purpose-/Beschreibungstexte in die Sidebar verlagert. +- Nutzt weiterhin die bestehende `shared/progress-stepper/`-Komponente (kein Neubau). + +--- + +## A13 – Schrittlogik klären und neu ordnen +**Typ:** Konzept · **Miro:** Aufbereitung Rework, Neuer Schritt 4 Anpassung · **Status:** In Feature-Branch · **Branch:** `feat/a13-schrittlogik` +**Befund:** K-02 + +**Problem:** Aufgabe der Schritte 2/3/4 und Bedeutung der jeweiligen Spaltenauswahl konzeptionell nicht getrennt. + +**Wo umsetzen:** +- Schrittbeschreibungen/mentales Modell: `shared/constants/step-info.ts`. +- Neuzuschnitt Schritt 4 (Spaltenauswahl neben Projektionsmethode): `steps/step4-visualization-settings/`. +- Abgrenzung Schritt 2 (betrachtete Spalten) vs. Schritt 3 (in Dimensionsreduktion): `steps/step2-column-selection/`, `steps/step3-configure-data-features/`. + +**Umgesetzt (PR #73, `feat/a13-schrittlogik`):** +- Auswahl „welche Spalten fließen in die Projektion" aus Schritt 3 entfernt; Schritt 3 ist jetzt reine Datenkonfiguration (keine „Include in projection"-Checkboxen/-Zähler mehr). +- Neue Section „Projection Options" in `steps/step4-visualization-settings/` zweispaltig: links Panel „Projection Columns" (Checkbox-Liste je aktiver Spalte, Typ-Badge, Zähler „x / n included", Select-all/Clear, Suche), rechts das bestehende Methoden-Grid. +- Schrittbeschreibungen in `shared/constants/step-info.ts` geschärft (Schritt 2 = betrachtete Spalten, Schritt 3 = Datenkonfiguration, Schritt 4 = Projektion: Spalten + Methode). +- Validierung in Schritt 4: mindestens eine Projektionsspalte erforderlich (sonst Warnung + Weiter deaktiviert); `includeInProjection` lag bereits im Service (keine Datenfluss-/Modelländerung). + +--- + +## A14 – Übergänge zwischen den Schritten sichtbar machen +**Typ:** Konzept · **Miro:** Animation · **Status:** In Feature-Branch · **Branch:** `feat/a14-animation` +**Befund:** K-03 + +**Problem:** Zusammenhang der Tabellen von Schritt 2 und 3 nicht sichtbar; unklar, wie die Auswahl übernommen wird. + +**Wo umsetzen:** +- Morphing-/Übergangsanimation Schritt 2↔3 (beidseitig): Übergang in `preprocessing-wizard.component.ts` orchestrieren, gemeinsame Tabellenkomponente `shared/data-preview-table/` als Träger der Animation. + +**Umgesetzt (PR #77, `feat/a14-animation`):** +- Morph-Übergang Schritt 2 → 3 in `preprocessing-wizard.component.ts` mit GSAP-Timelines orchestriert (globaler Speed-Faktor `MORPH_TIMESCALE`). +- Zwei Phasen: Phase 1 kollabiert die live Step-2-Tabelle per Transform-Overlay auf Rail-Breite (kein Layout-Ruckeln), dann Component-Swap; Phase 2 fährt Rail-Header (`yPercent`) und Detail-/Konfig-Well (`xPercent`) von außen herein. +- `prefers-reduced-motion: reduce` schaltet den Morph ab (sofortiger Wechsel). +- Einschränkung ggü. Ticket: nur der Vorwärts-Morph 2 → 3 ist animiert, nicht bidirektional (2↔3). + +--- + +## A15 – Einheitliches Tabellen- und Listen-Designsystem +**Typ:** Konzept · **Miro:** Tabellen/Listen · **Status:** In Feature-Branch · **Branch:** `feat/a15-table-system` +**Erweiterter Umfang:** zusätzlich Schritt-3-Redesign (Option 1d: Master-Rail + eingelassenes Konfigurations-Well mit Breadcrumb und Fehlwert-Badges) sowie Überarbeitung der Schritt-4-Feature-Liste (funktionale Suche/Typ-Filter, Drag-and-drop-Reihenfolge, Drop-in aus der Available-Liste, Höhenlimit mit Scroll) — Letzteres überschneidet sich mit A9. +**Befund:** K-04 + +**Problem:** Wiederkehrende Tabellen/Listen (Spaltenauswahl, Datenkonfiguration, Feature-Auswahl) sind funktional gleich, aber uneinheitlich gestaltet. + +**Wo umsetzen:** +- Gemeinsames Muster in `shared/data-preview-table/` konsolidieren (Auswahl links, Umschalter „alle", Suchfeld, keine Typ-Farbcodierung, einheitliche blaue Akzentfarbe). +- Anwendung in `steps/step2-column-selection/`, `steps/step3-configure-data-features/`, `steps/step4-visualization-settings/`. +- Gemeinsame Styles: `shared/_wizard-shared.scss`. + +**Umgesetzt (PR #75, `feat/a15-table-system`):** +- Gemeinsames Auswahl-Designsystem als geteilte Mixins in `shared/_wizard-shared.scss`, angewendet in Step 2/3/4: Auswahl-Checkbox links, Tri-State-„Select all" im Kopf (kein Extra-Button/Doppelzähler), Suchfeld, keine Typ-Farbcodierung der Zeilen; einheitliche Checkbox-Geometrie auf gemeinsamer vertikaler Linie. +- Einheitlicher Akzent aus `_variables.scss` statt neuem Token: `$active-color` (#00bcd4) für Auswahl/Aktiv, `$status-info` (#00838f) für den Auswahl-Zähler „N of M selected". +- Schritt-3-Redesign (Option 1d): Master-Rail links + eingelassenes Konfig-Well mit Breadcrumb; Duplikat-Vorschau nutzt die gemeinsame Tabellenkomponente. +- Schritt-4-Feature-Liste überarbeitet (Überschneidung mit A9): funktionale Suche/Typ-Filter, Drag-and-drop-Reihenfolge, Drop-in aus der Available-Liste, Höhenlimit mit Scroll, Reihenfolge-Badges in Cyan. +- Bewusst offengelassen: `data-type-badge`-Farben bleiben; die exakte Rot/Amber-Abstufung der Fehlwert-Badges wurde nicht nachgebaut. + +--- + +## 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** — 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 0c139c4..6824db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,46 +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", "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": { @@ -274,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": { @@ -293,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": { @@ -321,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", @@ -340,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": { @@ -350,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", @@ -482,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": { @@ -508,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", @@ -557,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" ], @@ -1319,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" ], @@ -1336,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", @@ -1444,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", @@ -1461,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" ], @@ -1530,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" ], @@ -1547,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" ], @@ -1564,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" ], @@ -1581,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" ], @@ -1598,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" ], @@ -1666,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": { @@ -1716,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": { @@ -1728,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" }, @@ -1764,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": { @@ -1805,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": { @@ -1860,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" @@ -2477,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", @@ -2745,6 +3128,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2762,6 +3148,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2779,6 +3168,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2796,6 +3188,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2813,6 +3208,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2830,6 +3228,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2847,6 +3248,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2943,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": { @@ -2999,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": { @@ -3052,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": { @@ -3535,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", @@ -4114,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": { @@ -4153,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": { @@ -4163,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": { @@ -4205,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", @@ -4274,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": { @@ -4568,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", @@ -4589,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", @@ -4603,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", @@ -4621,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": { @@ -4643,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" @@ -4678,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" @@ -4702,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" }, @@ -4737,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" }, @@ -4754,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" }, @@ -4780,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" @@ -4803,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" }, @@ -4825,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" @@ -4892,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" @@ -4915,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" @@ -4936,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": { @@ -5560,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" } @@ -5627,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", @@ -5671,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", @@ -5691,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": { @@ -5735,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" }, @@ -5781,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": [ { @@ -5827,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" @@ -5898,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": { @@ -5949,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": [ { @@ -5969,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", @@ -6182,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": { @@ -6381,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", @@ -6801,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", @@ -6855,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", @@ -6877,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", @@ -6979,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" }, @@ -7002,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", @@ -7133,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", @@ -7160,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", @@ -7174,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", @@ -7187,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": { @@ -7246,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": { @@ -7257,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", @@ -7378,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": { @@ -7486,1834 +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/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", @@ -9491,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", @@ -9608,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", @@ -9645,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", @@ -9662,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": { @@ -9702,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": { @@ -9724,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", @@ -9822,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" @@ -9837,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", @@ -9980,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", @@ -10055,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": [ { @@ -10081,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": { @@ -10099,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": { @@ -10150,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": { @@ -10176,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", @@ -10198,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", @@ -10346,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", @@ -10480,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": { @@ -10493,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", @@ -10512,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" @@ -10624,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", @@ -10662,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": { @@ -10682,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", @@ -10703,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": { @@ -10725,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": [ { @@ -10746,7 +9718,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -10824,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": { @@ -10850,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" @@ -10897,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", @@ -10965,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", @@ -10985,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", @@ -11019,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", @@ -11047,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", @@ -11167,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": { @@ -11210,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", @@ -11376,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" }, @@ -11451,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", @@ -11465,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", @@ -11523,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": { @@ -11704,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" }, @@ -11723,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", @@ -11733,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", @@ -11746,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", @@ -11831,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": { @@ -11858,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": { @@ -11890,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": { @@ -11923,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", @@ -12294,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": [ { @@ -12344,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", @@ -12374,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": { @@ -12422,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", @@ -12510,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" @@ -12623,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": { @@ -12669,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 bc8d348..f85a505 100644 --- a/package.json +++ b/package.json @@ -7,54 +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", "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}": [ @@ -68,4 +62,4 @@ "prettier --write" ] } -} \ No newline at end of file +} 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 e608361..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 @@ -42,6 +43,18 @@ export interface ProcessingProgress { message: string; } +/** + * 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. "Undo: Recommended features applied (Ctrl+Z)"). + */ +export interface HistoryStatus { + canUndo: boolean; + canRedo: boolean; + undoLabel: string | null; // label of the action that Undo would revert + redoLabel: string | null; // label of the action that Redo would re-apply +} + export const DEFAULT_CLEANING_CONFIG: CleaningConfig = { removeDuplicates: false, }; @@ -76,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 eef2e22..e10cdbc 100644 --- a/src/app/preprocessing-wizard/preprocessing-wizard.component.html +++ b/src/app/preprocessing-wizard/preprocessing-wizard.component.html @@ -1,50 +1,154 @@ -
    -
    -
    -

    Data Preprocessing Wizard

    -
    - - -
    + +
    +
    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 93c3988..0000000 --- a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html +++ /dev/null @@ -1,264 +0,0 @@ -
    -
    -
    -

    {{ stepInfo.title }}

    -

    {{ stepInfo.purpose }}

    -
    - - - - - @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(', ') }} -
    -
    -
    - - -
    - - - - - - - - - - - - @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 (showProcessing) { - @if (isProcessing) { - -
    -
    -
    - sync -
    - -

    Processing Your Data

    -

    {{ processingStep || 'Applying your preprocessing configuration...' }}

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

    Processing Complete!

    -

    Your dataset has been successfully processed and is ready for visualization.

    - -
    -
    - check - {{ enabledColumns }} columns processed -
    -
    - check - FastMap projection ready -
    -
    - check - Data cleaning applied -
    -
    - - - @if (getBackgroundProjectionsArray().length > 0) { -
    -

    - schedule - Background Projections -

    -
    - @for (proj of getBackgroundProjectionsArray(); track proj.method) { -
    -
    - {{ proj.method.toUpperCase() }} - @if (proj.status === 'running') { - - sync - Running - - } - @if (proj.status === 'complete') { - - check_circle - Complete - - } - @if (proj.status === 'error') { - - error - Error - - } -
    - @if (proj.status === 'running') { -
    -
    -
    -

    {{ proj.message }}

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

    Available in Glyphspace dropdown

    - } -
    - } -
    -
    - } -
    -
    - } - - @if (error) { - -
    - error -
    Error: {{ error }}
    -
    - } - } -
    - - - @if (!showProcessing) { -
    - - -
    - -
    -
    - } - - @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 059319f..0000000 --- a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss +++ /dev/null @@ -1,305 +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; - - .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; - } - } - } - } - } - } - - .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/step5-review-processing/step5-review-processing.component.ts b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts deleted file mode 100644 index b6edf37..0000000 --- a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { Component, OnInit, OnDestroy, ChangeDetectorRef, NgZone, Output, EventEmitter } from '@angular/core'; -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 { ToastService } from '../../../services/toast.service'; -import { ColumnConfig, ProjectionConfig } from '../../models/column-config'; -import { DataType, getEncodingLabel as encLabelFn, getScalingLabel as scaleLabelFn } from '../../models/data-type.enum'; -import { STEP_INFO } from '../../shared/constants/step-info'; -import { DataTypeBadgeComponent } from '../../shared/data-type-badge/data-type-badge.component'; - -@Component({ - selector: 'app-step5-review-processing', - standalone: true, - imports: [DataTypeBadgeComponent], - templateUrl: './step5-review-processing.component.html', - styleUrl: './step5-review-processing.component.scss', -}) -export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { - @Output() wizardFinish = new EventEmitter(); - getEncodingLabel = encLabelFn; - getScalingLabel = scaleLabelFn; - - // Review/Summary data - totalColumns = 0; - enabledColumns = 0; - projectionColumns = 0; - enabledMethods: string[] = []; - columnConfigs: ColumnConfig[] = []; - colorFeature: string | null = null; - selectedGlyphFeatures: string[] = []; - projectionConfig!: ProjectionConfig; - - // Processing state - isProcessing = false; - processingProgress = 0; - processingStep = ''; - processingComplete = false; - error: string | null = null; - showProcessing = false; - - // Background projection status - backgroundProjections = new Map(); - - // Capture dataset info for background projections (survives wizard reset) - private backgroundDatasetName = ''; - private backgroundTimestamp = ''; - - // Expose enums - DataType = DataType; - - readonly stepInfo = STEP_INFO[4]; // Step 5 (index 4) - - private progressSubscription?: Subscription; - private backgroundStatusSubscription?: Subscription; - - constructor( - public preprocessingService: PreprocessingService, - private dataLoader: DataLoaderService, - private projectionService: ProjectionService, - private toastService: ToastService, - private cdr: ChangeDetectorRef, - private ngZone: NgZone - ) {} - - ngOnInit(): void { - const state = this.preprocessingService.currentState; - - // Load color feature - const colorCol = Array.from(state.columnConfigs.values()).find(c => c.isColorFeature); - this.colorFeature = colorCol?.name ?? null; - - // Load glyph features - this.selectedGlyphFeatures = [...state.glyphFeatures]; - - // Load projection config - this.projectionConfig = { ...state.projectionConfig }; - - // Prepare review data - this.prepareReviewData(); - } - - ngOnDestroy(): void { - if (this.progressSubscription) { - this.progressSubscription.unsubscribe(); - } - if (this.backgroundStatusSubscription) { - this.backgroundStatusSubscription.unsubscribe(); - } - } - - // ============================================================================ - // Review/Summary - // ============================================================================ - - prepareReviewData(): void { - const state = this.preprocessingService.currentState; - - this.totalColumns = state.dataProfile?.columns.length || 0; - this.columnConfigs = Array.from(state.columnConfigs.values()); - this.enabledColumns = this.columnConfigs.filter(c => c.enabled).length; - 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'); - } - - // ============================================================================ - // Processing - // ============================================================================ - - private resetProcessingState(): void { - this.processingComplete = false; - this.isProcessing = false; - this.processingProgress = 0; - this.processingStep = ''; - this.error = null; - this.showProcessing = false; - } - - private updateProcessingUI(step: string, progress: number): void { - this.ngZone.run(() => { - this.processingStep = step; - this.processingProgress = progress; - this.cdr.detectChanges(); - }); - } - - async startProcessing(): Promise { - this.resetProcessingState(); - this.showProcessing = true; - this.isProcessing = true; - this.processingStep = 'Initializing...'; - - this.progressSubscription = this.preprocessingService.processingProgress.subscribe({ - next: progress => { - this.processingStep = progress.message || progress.step; - this.processingProgress = Math.min(progress.progress, 70); - this.cdr.detectChanges(); - }, - }); - - try { - await this.preprocessingService.processData(); - - this.updateProcessingUI('Loading features for projections...', 70); - - const csvText = await this.preprocessingService.getProcessedFeaturesCSV(); - const { features, ids } = this.projectionService.parseCSVFeatures(csvText); - - this.updateProcessingUI('Computing FastMap projection...', 75); - - // Use FastMap as the primary projection - const fastmapResult = await this.projectionService.runFastMapSync(features, ids); - - this.updateProcessingUI('Loading dataset with FastMap...', 90); - - await this.preprocessingService.addProjectionPositions('fastmap', fastmapResult.positions); - - 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.startBackgroundProjections(features, ids); - } catch (error: unknown) { - console.error('Processing failed:', error); - this.ngZone.run(() => { - this.error = error instanceof Error ? error.message : 'Processing failed'; - this.isProcessing = false; - this.cdr.detectChanges(); - }); - } finally { - if (this.progressSubscription) { - this.progressSubscription.unsubscribe(); - this.progressSubscription = undefined; - } - } - } - - private async startBackgroundProjections(features: number[][], ids: (string | number)[]): Promise { - const config = this.projectionConfig; - - // Capture dataset info so background projections can add positions even after wizard reset - const state = this.preprocessingService.currentState; - this.backgroundDatasetName = state.datasetName; - this.backgroundTimestamp = state.timestamp; - - this.backgroundStatusSubscription = this.projectionService.backgroundStatusObservable.subscribe(statusMap => { - this.ngZone.run(() => { - this.backgroundProjections.clear(); - statusMap.forEach((status, method) => { - this.backgroundProjections.set(method, { - status: status.status, - progress: status.progress, - message: status.message, - }); - }); - this.cdr.detectChanges(); - }); - }); - - // 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); - } - } - } - - private async runBackgroundProjection(name: string, computeFn: () => Promise): Promise { - try { - const result = await computeFn(); - - // Convert positions to the format expected by DataProvider - const positionsForProvider = result.positions.map(p => ({ - id: p.id, - position: { x: p.x, y: p.y }, - })); - - // Try to add to wizard state first (if still active) - await this.preprocessingService.addProjectionPositions(result.method, result.positions); - - const state = this.preprocessingService.currentState; - if (state.processedDataset) { - // Wizard still active - update via normal flow - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- processedDataset is an opaque structure from Python processing - const collection = state.processedDataset as any; - const datasetKey = - collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); - - if (datasetKey && collection.datasets) { - const dataset = collection.datasets[datasetKey]; - if (dataset) { - this.dataLoader.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); - this.dataLoader.loadProcessedDataset(dataset, state.datasetName, state.timestamp); - } - } - } else if (this.backgroundDatasetName && this.backgroundTimestamp) { - // Wizard was reset but dataset is already loaded in dashboard - this.dataLoader.addPositionsToLoadedDataset( - this.backgroundDatasetName, - this.backgroundTimestamp, - result.method, - positionsForProvider - ); - // Re-save to IndexedDB with the new projection included - this.dataLoader.saveDatasetToStorage(this.backgroundDatasetName, this.backgroundTimestamp); - } - - this.ngZone.run(() => { - this.toastService.success(`${name} projection ready! (${(result.computeTime / 1000).toFixed(1)}s)`, 4000); - }); - } catch (error: unknown) { - console.error(`${name} projection failed:`, error); - this.ngZone.run(() => { - this.toastService.error( - `${name} projection failed: ${error instanceof Error ? error.message : String(error)}`, - 6000 - ); - }); - } - } - - goToDashboard(): void { - const state = this.preprocessingService.currentState; - - if (state.processedDataset) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- processedDataset is an opaque structure from Python processing - const collection = state.processedDataset as any; - const datasetKey = - collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); - - if (!datasetKey || !collection.datasets) { - this.error = 'Invalid dataset structure. Please try processing again.'; - return; - } - - const dataset = collection.datasets[datasetKey]; - - if (dataset) { - this.dataLoader.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); - this.dataLoader.loadProcessedDataset(dataset, state.datasetName, state.timestamp); - // Persist to IndexedDB for cross-session survival - this.dataLoader.saveDatasetToStorage(state.datasetName, state.timestamp); - } else { - this.error = 'Failed to load processed dataset'; - return; - } - } - - // Reset wizard state so it's ready for a new upload - this.preprocessingService.resetState(); - - this.wizardFinish.emit(); - } - - goBack(): void { - this.preprocessingService.previousStep(); - } - - startOver(): void { - if (confirm('Are you sure you want to start over? All current configuration will be lost.')) { - // Terminate any running background projection workers - this.projectionService.terminateAllWorkers(); - this.projectionService.clearBackgroundStatuses(); - - // Clear local state - this.backgroundProjections.clear(); - this.resetProcessingState(); - - // Reset wizard state - this.preprocessingService.resetState(); - this.preprocessingService.goToStep(0); - } - } - - 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; - } -} 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/step6-review-processing/step6-review-processing.component.ts b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.ts new file mode 100644 index 0000000..7929ce9 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step6-review-processing/step6-review-processing.component.ts @@ -0,0 +1,719 @@ +import { Component, OnInit, OnDestroy, ChangeDetectorRef, NgZone, Output, EventEmitter } from '@angular/core'; +import { NgTemplateOutlet } from '@angular/common'; +import { Subscription } from 'rxjs'; +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 { 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 { 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, + 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: 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-step6-review-processing', + standalone: true, + imports: [NgTemplateOutlet, DataTypeBadgeComponent, WizardIssueCardComponent], + templateUrl: './step6-review-processing.component.html', + styleUrl: './step6-review-processing.component.scss', +}) +export class Step6ReviewProcessingComponent implements OnInit, OnDestroy { + @Output() wizardFinish = new EventEmitter(); + getEncodingLabel = encLabelFn; + getScalingLabel = scaleLabelFn; + + // Review/Summary data + totalColumns = 0; + enabledColumns = 0; + projectionColumns = 0; + enabledMethods: string[] = []; + columnConfigs: ColumnConfig[] = []; + colorFeature: string | null = null; + colorScaleName = ''; + selectedGlyphFeatures: string[] = []; + projectionConfig: ProjectionConfig = { ...DEFAULT_PROJECTION_CONFIG }; + + // Processing state + isProcessing = false; + processingProgress = 0; + processingStep = ''; + processingComplete = false; + error: string | null = null; + showProcessing = false; + + // 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(); + + // Capture dataset info for background projections (survives wizard reset) + private backgroundDatasetName = ''; + private backgroundTimestamp = ''; + + // Expose enums + DataType = DataType; + + 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 announcer: WizardAnnouncer + ) {} + + ngOnInit(): void { + const state = this.preprocessingService.currentState; + + // 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]; + + // Load projection config + this.projectionConfig = { ...state.projectionConfig }; + + // Prepare review data + this.prepareReviewData(); + + // A3: pre-flight — surface likely failures from signals already in the state. + this.preflightIssues = detectPreflightIssues(state); + + // A3 / A11 (P-S5-02, P-S5-03): restore the persistent result/status on re-entry. + // The service is a root singleton, so background statuses and the processed + // dataset survive closing the wizard. Re-subscribe to keep the method bar and + // partial-failure detection alive after reopening. + this.backgroundStatusSubscription = this.projectionService.backgroundStatusObservable.subscribe(statusMap => { + this.ngZone.run(() => { + this.syncBackgroundStatus(statusMap); + this.cdr.detectChanges(); + }); + }); + + if (state.error) { + // A previous run failed and the error survived in the singleton state. + // Show it as a persistent, classified error screen instead of a stale banner. + this.blockingIssue = classifyProcessingError(state.error); + this.showProcessing = true; + this.isProcessing = false; + this.processingComplete = false; + } else if (state.processedDataset) { + // Processing already finished in an earlier session of this wizard instance: + // restore the success/result screen rather than the "Start Processing" view. + this.showProcessing = true; + this.processingComplete = true; + this.isProcessing = false; + } + } + + ngOnDestroy(): void { + this.destroyed = true; + if (this.progressSubscription) { + this.progressSubscription.unsubscribe(); + } + if (this.backgroundStatusSubscription) { + this.backgroundStatusSubscription.unsubscribe(); + } + } + + // ============================================================================ + // Review/Summary + // ============================================================================ + + prepareReviewData(): void { + const state = this.preprocessingService.currentState; + + this.totalColumns = state.dataProfile?.columns.length || 0; + this.columnConfigs = Array.from(state.columnConfigs.values()); + this.enabledColumns = this.columnConfigs.filter(c => c.enabled).length; + this.projectionColumns = this.columnConfigs.filter(c => c.enabled && c.includeInProjection).length; + + // FastMap is always the primary projection + this.enabledMethods = ['FastMap (Primary)', ...enabledBackgroundMethodNames(this.projectionConfig)]; + } + + // ============================================================================ + // Processing + // ============================================================================ + + private resetProcessingState(): void { + this.processingComplete = false; + this.isProcessing = false; + this.processingProgress = 0; + this.processingStep = ''; + this.error = null; + this.showProcessing = false; + this.blockingIssue = null; + this.partialIssues = []; + this.dismissedMethods.clear(); + this.lastAnnouncedPhase = ''; + // Clear any stale error left in the singleton state (P-S5-03). + this.preprocessingService.clearError(); + } + + private updateProcessingUI(step: string, progress: number): void { + this.ngZone.run(() => { + this.processingStep = step; + this.processingProgress = progress; + 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; + + /** Resolve after the browser has had a chance to paint, so a state change + * made right before heavy work is rendered first. */ + private paintYield(): Promise { + return new Promise(resolve => requestAnimationFrame(() => setTimeout(resolve, 0))); + } + + /** Hold until the loading hero has been visible for at least MIN_LOADING_MS. */ + private async ensureMinVisible(startedAt: number): Promise { + const remaining = this.MIN_LOADING_MS - (performance.now() - startedAt); + if (remaining > 0) { + await new Promise(resolve => setTimeout(resolve, remaining)); + } + } + + 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?.unsubscribe(); + const progressSubscription = this.preprocessingService.processingProgress.subscribe({ + next: progress => { + this.processingStep = progress.message || progress.step; + this.processingProgress = Math.min(progress.progress, 70); + // 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. + const startedAt = performance.now(); + await this.paintYield(); + + try { + // 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; + } + } + + /** + * 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(); + } + } + + /** + * 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('Loading features for projections...', 70); + + const csvText = await this.preprocessingService.getProcessedFeaturesCSV(); + const { features, ids } = this.projectionService.parseCSVFeatures(csvText); + + this.updateProcessingUI('Computing FastMap projection...', 75); + + // Use FastMap as the primary projection + const fastmapResult = await this.projectionService.runFastMapSync(features, ids); + + this.updateProcessingUI('Loading dataset with FastMap...', 90); + + 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 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; + this.backgroundTimestamp = state.timestamp; + + // Note: the background-status subscription is created once in ngOnInit so it + // also restores the method bar after the wizard is reopened. + const runs = buildBackgroundProjectionRuns(this.projectionService, this.projectionConfig, features, ids); + for (const { name, run } of runs) { + void this.runBackgroundProjection(name, run); + } + } + + private async runBackgroundProjection(name: string, computeFn: () => Promise): Promise { + try { + const result = await computeFn(); + + // Convert positions to the format expected by DataProvider + const positionsForProvider = result.positions.map(p => ({ + id: p.id, + position: { x: p.x, y: p.y }, + })); + + // Try to add to wizard state first (if still active) + await this.preprocessingService.addProjectionPositions(result.method, result.positions); + + const state = this.preprocessingService.currentState; + if (state.processedDataset) { + // Wizard still active - update via normal flow + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- processedDataset is an opaque structure from Python processing + const collection = state.processedDataset as any; + const datasetKey = + collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); + + if (datasetKey && collection.datasets) { + const dataset = collection.datasets[datasetKey]; + if (dataset) { + this.dataLoader.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); + this.dataLoader.loadProcessedDataset(dataset, state.datasetName, state.timestamp); + } + } + } else if (this.backgroundDatasetName && this.backgroundTimestamp) { + // Wizard was reset but dataset is already loaded in dashboard + this.dataLoader.addPositionsToLoadedDataset( + this.backgroundDatasetName, + this.backgroundTimestamp, + result.method, + positionsForProvider + ); + // Re-save to IndexedDB with the new projection included + this.dataLoader.saveDatasetToStorage(this.backgroundDatasetName, this.backgroundTimestamp); + } + + this.ngZone.run(() => { + 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(() => { + // A3 review fix: never surface the raw technical message in the toast. + // Use the classified, everyday-language title; the full WHY/FIX and the + // raw detail live in the partial-issue card's collapsible section. + const issue = classifyBackgroundFailure(name, raw); + this.toastService.error(issue.title, 6000); + }); + } + } + + goToDashboard(): void { + const state = this.preprocessingService.currentState; + + if (state.processedDataset) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- processedDataset is an opaque structure from Python processing + const collection = state.processedDataset as any; + const datasetKey = + collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); + + if (!datasetKey || !collection.datasets) { + this.error = 'Invalid dataset structure. Please try processing again.'; + return; + } + + const dataset = collection.datasets[datasetKey]; + + if (dataset) { + this.dataLoader.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); + this.dataLoader.loadProcessedDataset(dataset, state.datasetName, state.timestamp); + // Persist to IndexedDB for cross-session survival + this.dataLoader.saveDatasetToStorage(state.datasetName, state.timestamp); + } else { + this.error = 'Failed to load processed dataset'; + return; + } + } + + // 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(); + + this.wizardFinish.emit(); + } + + goBack(): void { + this.preprocessingService.previousStep(); + } + + // A6: jump from a review summary card back to the step that owns that setting + // and scroll the relevant section into view. + editSetting(step: WizardStepId, targetId: string): void { + this.preprocessingService.goToStepWithScroll(step, targetId); + } + + startOver(): void { + if (confirm('Are you sure you want to start over? All current configuration will be lost.')) { + // Terminate any running background projection workers + this.projectionService.terminateAllWorkers(); + this.projectionService.clearBackgroundStatuses(); + + // Clear local state + this.backgroundProjections = new Map(); + this.backgroundItems = []; + this.resetProcessingState(); + + // Reset wizard state + this.preprocessingService.resetState(); + this.preprocessingService.goToStep('upload'); + } + } + + // ============================================================================ + // 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 + // ============================================================================ + + /** 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. */ + get isPartialResult(): boolean { + return this.processingComplete && !this.isProcessing && this.partialIssues.length > 0; + } + + /** True when the primary run failed (hard, blocking error screen). */ + get isErrorScreen(): boolean { + return !!this.blockingIssue && !this.isProcessing; + } + + /** Method status bar (FastMap primary + enabled background methods). */ + get methodStatuses(): MethodStatus[] { + return this.enabledMethods.map(label => { + const status = label.startsWith('FastMap') ? this.primaryChipStatus() : this.backgroundChipStatus(label); + return { + name: label.replace(' (Primary)', ''), + role: methodRole(label), + status, + statusLabel: CHIP_LABELS[status], + }; + }); + } + + 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 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 ${number} · ${stepName}`, 2500); + } + + /** Dismiss a partial (optional) issue so the degraded notice can be cleared. */ + dismissIssue(issue: WizardIssue): void { + const method = this.getMethodForPartialIssue(issue); + if (method) this.dismissedMethods.add(method); + this.partialIssues = this.partialIssues.filter(i => i !== issue); + } + + private getMethodForPartialIssue(issue: WizardIssue): string | null { + // Titles are " could not be computed". + const match = issue.title.match(/^(\S+) could not be computed/); + return match ? match[1].toLowerCase() : null; + } + + /** + * Retry / reprocess, keeping all uploaded data and settings. Reachable from + * both the blocking-error screen and the success screen. + * + * A3 review fix: the earlier version only called startProcessing(), so a fast + * run looked like a brief flash with no confirmation that anything happened. + * We now announce the retry with a toast and force the processing hero to show + * (isProcessing/showProcessing) before the async work begins, so the user + * clearly sees the spinner + progress + method status while it re-runs. + */ + retryProcessing(): void { + this.toastService.info('Restarting processing…', 3000); + // Make the processing state visible immediately, even before startProcessing's + // own reset runs, so the transition is never just a flicker. + this.showProcessing = true; + this.isProcessing = true; + this.processingComplete = false; + this.blockingIssue = null; + this.processingStep = 'Restarting processing…'; + this.cdr.detectChanges(); + this.startProcessing(); + } +} 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 09acf2f..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,10 +230,16 @@ export class DataProcessorService { }, timeoutMs); const sub = this.message$.subscribe(msg => { + if (msg.requestId !== requestId) { + return; + } if (msg.type === 'error') { clearTimeout(timer); sub.unsubscribe(); - reject(msg.message); + // Wrap the worker message in an Error so the real cause survives the + // `instanceof Error` check downstream (was rejected as a bare string, + // which forced the generic "Processing failed" fallback). + reject(new Error(typeof msg.message === 'string' ? msg.message : 'Worker reported an error')); } else if (msg.type === type && matchFn(msg as T)) { clearTimeout(timer); sub.unsubscribe(); @@ -224,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 475d2dc..bc1e553 100644 --- a/src/app/services/toast.service.ts +++ b/src/app/services/toast.service.ts @@ -1,11 +1,36 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; +export interface ToastAction { + label: string; + handler: () => void; +} + export interface Toast { id: number; message: string; type: 'success' | 'error' | 'info' | 'warning'; duration?: number; + 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({ @@ -15,26 +40,44 @@ 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): 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 "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: () => 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 { this.show(message, 'success', duration); } @@ -51,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 `