diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1083565..4c00886 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -23,15 +23,20 @@ jobs: - name: Install docs dependencies run: python -m pip install -r tools/requirements/docs.txt + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Setup Node uses: actions/setup-node@v4 with: node-version: "22" - cache: npm - cache-dependency-path: frontend-scratch-v2/package-lock.json + cache: pnpm + cache-dependency-path: frontend-v3/pnpm-lock.yaml - name: Install frontend dependencies - run: npm --prefix frontend-scratch-v2 ci + run: pnpm --dir frontend-v3 install --frozen-lockfile - name: Run canonical content checks run: bash tools/test/run_content_checks.sh @@ -39,14 +44,14 @@ jobs: - name: Validate runtime bundle run: python tools/scenario/validate_runtime_bundle.py - - name: Lint studio frontend - run: npm --prefix frontend-scratch-v2 run lint + - name: Typecheck frontend + run: pnpm --dir frontend-v3 typecheck - - name: Test studio frontend - run: npm --prefix frontend-scratch-v2 run test + - name: Test frontend + run: pnpm --dir frontend-v3 test - - name: Build studio frontend - run: npm --prefix frontend-scratch-v2 run build + - name: Build frontend + run: pnpm --dir frontend-v3 build - name: Build docs run: python -m mkdocs build --strict diff --git a/CLAUDE.md b/CLAUDE.md index b1a83fb..3b00baa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ YAML scenario → compile_runtime3.py → Runtime 3 IR → ESP32 / Web player Key surfaces: - **Scenario IR**: `game/scenarios/zacus_v2.yaml` → `tools/scenario/compile_runtime3.py` → portable Runtime 3 IR. Contract: `specs/ZACUS_RUNTIME_3_SPEC.md`. -- **Authoring**: `frontend-scratch-v2/` (legacy single-app) and `frontend-v3/` (pnpm monorepo, dashboard + editor + simulation). +- **Authoring**: `frontend-v3/` (pnpm monorepo, dashboard + editor + simulation; editor + simulation will fuse into `apps/atelier/` per `docs/superpowers/specs/2026-05-01-v3-fusion-atelier-design.md`). - **Firmware**: `ESP32_ZACUS/` submodule (separate repo, separate CI). Freenove ESP32-S3 + PlatformIO. NPC engine, voice pipeline, vision/QR, media manager. - **Voice / NPC**: Piper TTS on Tower:8001 (zacus voice = tom-medium). NPC phrases in `game/scenarios/npc_phrases.yaml`. MP3 pool generator: `tools/tts/generate_npc_pool.py`. - **MCP hardware**: `tools/dev/mcp_hardware_server.py` (stdio, 6 tools). @@ -52,8 +52,7 @@ Key surfaces: |------|----------| | Edit scenarios, NPC phrases, prompts | `game/` | | Modify Runtime 3 compiler / validators / TTS pool / dev CLI | `tools/` | -| Authoring UI (legacy) | `frontend-scratch-v2/` | -| Authoring UI / dashboard / simulation (current) | `frontend-v3/` | +| Authoring UI / dashboard / simulation | `frontend-v3/` | | Zacus Studio macOS app | `desktop/` | | Add or change a contract spec | `specs/` | | Python tests (Runtime 3, NPC) | `tests/` | @@ -87,4 +86,4 @@ User speaks French → respond in French. Code, comments, commits, docs → Engl Domain-specific rules live in nested `CLAUDE.md` files and load automatically when you read files in those directories. Closest file wins. Current nested: - `game/`, `tools/`, `tests/`, `specs/` -- `frontend-scratch-v2/`, `frontend-v3/`, `desktop/` +- `frontend-v3/`, `desktop/` diff --git a/CLAUDE.md.backup b/CLAUDE.md.backup new file mode 100644 index 0000000..33d2869 --- /dev/null +++ b/CLAUDE.md.backup @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +**Le Mystère du Professeur Zacus** — hybrid educational escape room game with ESP32-S3 hardware, React authoring studio, and a portable Runtime 3 scenario engine. Scenarios are authored in YAML, compiled to a portable IR, and executed on both web and ESP32 targets. + +## Build & Test + +```bash +# Full validation pipeline +make all-validate + +# Content checks (schema validation for scenarios, audio, printables) +make content-checks + +# Runtime 3 — compile, simulate, verify, test +make runtime3-compile # YAML → Runtime 3 IR +make runtime3-simulate # simulate scenario execution +make runtime3-verify # verify pivot logic +make runtime3-test # Python unittest suite + +# Frontend (React 19 + Blockly + Vite) +cd frontend-scratch-v2 +npm test # Vitest (18 tests) +npm run build # tsc -b + vite build +npx vitest run tests/specific.test.ts # single test + +# Compile a specific scenario +python3 tools/scenario/compile_runtime3.py game/scenarios/zacus_v2.yaml + +# Dev CLI (12 actions) +./tools/dev/zacus.sh content-checks +./tools/dev/zacus.sh runtime3-compile +./tools/dev/zacus.sh voice-bridge start|stop|status|test +``` + +## Architecture + +### Data Flow +``` +YAML scenario → compile_runtime3.py → Runtime 3 IR → ESP32 / Web player + ↓ + Voice bridge → Piper TTS (Tower:8001) + ↓ + Hints engine → /hints/ask (3 puzzles, 3 levels) +``` + +### Key Components + +- **Scenario engine**: `game/scenarios/zacus_v2.yaml` is the canonical source. Runtime 3 spec in `specs/ZACUS_RUNTIME_3_SPEC.md`. +- **Frontend studio** (`frontend-scratch-v2/`): React 19 + Blockly 12.4 + Monaco Editor + Zod validation. Vite bundler, TypeScript. +- **ESP32 firmware** (`ESP32_ZACUS/` submodule): Freenove ESP32-S3, PlatformIO build. Separate repo with its own CI. Contains voice pipeline scaffold, audio/vision/QR detection, media manager. +- **Runtime 3 compiler** (`tools/scenario/compile_runtime3.py`): YAML → portable IR with pivots, zones, triggers. +- **Voice pipeline**: Piper TTS on Tower:8001 (3 voices: zacus=tom-medium, siwis, upmc), ESP-SR for wake word. Voice bridge routes `[HINT:puzzle:level]` to hints engine. +- **MCP hardware server** (`tools/dev/mcp_hardware_server.py`): 6 tools, stdio transport for hardware interaction. +- **TUI dashboard** (`tools/dev/zacus_tui.py`): 12 actions, logs, CI mode. +- **Analytics**: ESP32 module + 6 web endpoints + Dashboard UI. +- **NPC Engine** (`ESP32_ZACUS/ui_freenove_allinone/include/npc/npc_engine.h` + `src/npc/npc_engine.cpp`): Lightweight C state machine for Professor Zacus NPC. Trigger rules (stuck timer, QR scan, fast/slow progress, hint request), mood system (neutral/impressed/worried/amused), hybrid audio routing (live Piper TTS when Tower reachable, SD card MP3 fallback). NPC phrase bank in `game/scenarios/npc_phrases.yaml`. +- **TTS Client** (`ESP32_ZACUS/ui_freenove_allinone/include/npc/tts_client.h` + `src/npc/tts_client.cpp`): HTTP client for Piper TTS on Tower:8001 with health-check, PSRAM WAV buffer, and SD card fallback. Voice: tom-medium. +- **NPC Phrase Bank** (`game/scenarios/npc_phrases.yaml`): All Professor Zacus lines in French, organized by category: hints (3 levels × 6 scenes), congratulations, warnings, personality comments (by mood), adaptation phrases (skip/challenge/timer), narrative bridges, false leads, and ambiance (intro/outro/idle). +- **NPC MP3 Pool Generator** (`tools/tts/generate_npc_pool.py`): Python tool that reads `npc_phrases.yaml`, calls Piper TTS API for each phrase, writes MP3 files to `hotline_tts/`, and generates `hotline_tts/manifest.json`. Idempotent (skips already-generated files). Run: `python3 tools/tts/generate_npc_pool.py [--dry-run]`. + +### AI Integration +- 6 AI agent definitions in `.github/agents/` (voice, tts, vision, hints, audio_gen, mcp) +- Hints engine: anti-cheat, 3 difficulty levels, per-puzzle context +- Spec: `specs/AI_INTEGRATION_SPEC.md` + +## Canonical Files + +| File | Role | +|------|------| +| `game/scenarios/zacus_v2.yaml` | Scenario source of truth (v3, Runtime 3) | +| `game/scenarios/npc_phrases.yaml` | Professor Zacus NPC phrase bank (all categories, French) | +| `tools/tts/generate_npc_pool.py` | NPC MP3 pool generator (Piper TTS → hotline_tts/) | +| `specs/ZACUS_RUNTIME_3_SPEC.md` | Runtime contract definition | +| `specs/AI_INTEGRATION_SPEC.md` | AI layer architecture | +| `Makefile` | Main automation entry point | +| `docs/QUICKSTART.md` | Getting started | +| `docs/DEPLOYMENT_RUNBOOK.md` | Field deployment | +| `docs/debt/codex-firmware-fixes-to-apply.md` | Pending firmware fixes (I2S, CORS, AP password) | + +## Language & Communication + +- User speaks **French**, code and docs in **English** +- Respond in French for conversation, English for code/comments/commits + +## Infrastructure + +- **Tower** (`clems@192.168.0.120`): Piper TTS FR on port 8001 +- **KXKM-AI** (`kxkm@kxkm-ai`): RTX 4090, GPU inference +- SSH is key-based only, never use sshpass diff --git a/Makefile b/Makefile index de9afff..ad9de40 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ PYTHON ?= python3 SCENARIO ?= game/scenarios/zacus_v2.yaml -FRONTEND_DIR ?= frontend-scratch-v2 +FRONTEND_DIR ?= frontend-v3 -.PHONY: bootstrap-validators bootstrap-docs scenario-validate audio-validate printables-validate export validate-runtime-bundle content-checks runtime3-compile runtime3-simulate runtime3-verify runtime3-test runtime3-firmware-bundle frontend-lint frontend-test frontend-build docs-build docs-serve all-validate images +.PHONY: bootstrap-validators bootstrap-docs scenario-validate audio-validate printables-validate export validate-runtime-bundle content-checks runtime3-compile runtime3-simulate runtime3-verify runtime3-test runtime3-firmware-bundle frontend-typecheck frontend-test frontend-build docs-build docs-serve all-validate images bootstrap-validators: bash tools/setup/install_validators.sh @@ -43,14 +43,14 @@ runtime3-test: runtime3-firmware-bundle: $(PYTHON) tools/scenario/export_runtime3_firmware_bundle.py $(SCENARIO) -frontend-lint: - cd $(FRONTEND_DIR) && npm run lint +frontend-typecheck: + cd $(FRONTEND_DIR) && pnpm typecheck frontend-test: - cd $(FRONTEND_DIR) && npm run test + cd $(FRONTEND_DIR) && pnpm test frontend-build: - cd $(FRONTEND_DIR) && npm run build + cd $(FRONTEND_DIR) && pnpm build docs-build: $(PYTHON) -m mkdocs build --strict diff --git a/desktop/CLAUDE.md b/desktop/CLAUDE.md index 3f2f9ab..977bdf2 100644 --- a/desktop/CLAUDE.md +++ b/desktop/CLAUDE.md @@ -42,4 +42,3 @@ npm run rebuild-native # zacus-native addon for current Electron ABI - Calling `require('serialport')` from renderer (security: nodeIntegration must stay false) - Hardcoding device paths (`/dev/cu.usbserial-*`) — enumerate at runtime via `serialport.list()` - Skipping notarization on release builds (Gatekeeper will block) -- Bundling V2 frontend instead of V3 — V2 is dev-only diff --git a/frontend-scratch-v2/.gitignore b/frontend-scratch-v2/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/frontend-scratch-v2/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/frontend-scratch-v2/CLAUDE.md b/frontend-scratch-v2/CLAUDE.md deleted file mode 100644 index 062ab78..0000000 --- a/frontend-scratch-v2/CLAUDE.md +++ /dev/null @@ -1,47 +0,0 @@ -# Frontend Scratch V2 - -Authoring studio: React 19 + Blockly 12 + Monaco Editor + Zod + Vite 7 + Vitest 3. TypeScript strict. - -## Layout - -``` -src/ - App.tsx, main.tsx # Entry - components/ # UI (scenario editor, blocks, panels) - lib/ - api.ts # ESP32/runtime HTTP client - runtime3.ts # Runtime 3 IR types + compiler bindings (TS mirror of Python) - scenario.ts # Scenario YAML/JSON helpers - useRuntimeStore.ts # Zustand-style runtime state hook - __tests__/ # Co-located unit tests -tests/ - scenario-runtime3.test.ts # Integration test against real fixtures -``` - -## Commands - -```bash -npm run dev # Vite dev server -npm run build # tsc -b && vite build -npm test # Vitest run (18 tests) -npm run lint # ESLint flat config -``` - -## Patterns - -- `lib/runtime3.ts` is the TS mirror of Python `tools/scenario/runtime3_common.py` — keep field names + optionality identical. When the Python IR changes, update both in the same PR. -- Validate scenario imports through Zod schemas, not raw `JSON.parse` — Zod errors surface bad fixtures during dev. -- Blockly toolbox/blocks live in `components/scenario-editor/` — register custom blocks before `Blockly.inject`. -- Monaco editor language is YAML; load worker via Vite import, never CDN. - -## Tests - -- Vitest config inherits from `vite.config.ts`. Use `vitest run --reporter verbose` for diff output. -- Cross-stack invariant tests (Python compile → TS replay) live in `tests/scenario-runtime3.test.ts`. Run them locally before claiming Runtime 3 work is done. - -## Anti-Patterns - -- Drifting `lib/runtime3.ts` types from the Python compiler — schedule a sync, don't bandaid -- Calling `fetch()` outside `lib/api.ts` — keep network code centralised -- Using `any` to escape Zod typing — narrow with `z.infer` instead -- Importing from `frontend-v3/` packages — V2 and V3 share no runtime code diff --git a/frontend-scratch-v2/README.md b/frontend-scratch-v2/README.md deleted file mode 100644 index 4342930..0000000 --- a/frontend-scratch-v2/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Zacus Story Designer — React 19 + Blockly - -Studio auteur visuel pour concevoir des scenarios Zacus avec des blocs type Scratch. -Genere du YAML canonique et communique avec le firmware ESP32 via l'API Story V2. - -## Setup - -```bash -cd frontend-scratch-v2 -npm install -npm run dev -``` - -Connexion API (optionnel) : - -```bash -VITE_STORY_API_BASE=http://:8080 npm run dev -``` - -## Scripts disponibles - -| Commande | Description | -| --- | --- | -| `npm run dev` | Serveur de dev Vite (HMR) | -| `npm run build` | Build production (`dist/`) | -| `npm test` | Tests Vitest (18 tests) | -| `npm run lint` | ESLint | - -## Architecture - -L'interface s'organise en 4 onglets : - -| Onglet | Composant | Role | -| --- | --- | --- | -| Designer | `BlocklyDesigner.tsx` | Editeur blocs + generation YAML live | -| Dashboard | `Dashboard.tsx` | Vue d'ensemble scenario | -| Media | `MediaManager.tsx` | Gestion assets audio/image | -| Network | `NetworkPanel.tsx` | Monitoring devices terrain | - -Autres fichiers cles : -- `src/components/RuntimeControls.tsx` — actions HTTP Story V2 (list, status, validate, deploy) -- `src/lib/scenario.ts` — mapping blocs → document scenario → YAML -- `src/types.ts` — types TypeScript du document scenario - -## Stack OSS - -| Brique | Version | Licence | Role | -| --- | --- | --- | --- | -| blockly | 12.4.1 | Apache-2.0 | editeur blocs | -| yaml | 2.8.2 | ISC | serialisation YAML | -| zod | 4.3.6 | MIT | validation locale | -| ajv | 8.18.0 | MIT | JSON schema | -| @monaco-editor/react | 4.7.0 | MIT | vue YAML | - -Note : `scratch-gui` / `scratch-vm` sont AGPL-3.0, non retenus pour garder un front permissif. - -## API Story V2 - -| Methode | Endpoint | Description | -| --- | --- | --- | -| GET | `/api/story/list` | Liste des scenarios | -| GET | `/api/story/status` | Statut runtime | -| POST | `/api/story/validate` | Validation YAML | -| POST | `/api/story/deploy` | Deploiement sur device | - -Variable d'environnement : `VITE_STORY_API_BASE` (defaut : pas de proxy). - -## Tests - -18 tests passing (Vitest). Lancer avec `npm test`. - -## Prochaines etapes - -1. Mapping complet steps + transitions (FSM) avec validation croisee -2. Import/export Blockly JSON -3. Tests E2E avec API mock diff --git a/frontend-scratch-v2/eslint.config.js b/frontend-scratch-v2/eslint.config.js deleted file mode 100644 index 5e6b472..0000000 --- a/frontend-scratch-v2/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/frontend-scratch-v2/index.html b/frontend-scratch-v2/index.html deleted file mode 100644 index ea12759..0000000 --- a/frontend-scratch-v2/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - frontend-scratch-v2 - - -
- - - diff --git a/frontend-scratch-v2/package-lock.json b/frontend-scratch-v2/package-lock.json deleted file mode 100644 index 8fac41f..0000000 --- a/frontend-scratch-v2/package-lock.json +++ /dev/null @@ -1,4364 +0,0 @@ -{ - "name": "frontend-scratch-v2", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend-scratch-v2", - "version": "0.0.0", - "dependencies": { - "@monaco-editor/react": "^4.7.0", - "ajv": "^8.18.0", - "blockly": "^12.4.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "yaml": "^2.8.2", - "zod": "^4.3.6" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.1", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.48.0", - "vite": "^7.3.1", - "vitest": "^3.2.4" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "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==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "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==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "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==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "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==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "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==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "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==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "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==", - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@monaco-editor/loader": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", - "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", - "license": "MIT", - "dependencies": { - "state-local": "^1.0.6" - } - }, - "node_modules/@monaco-editor/react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", - "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", - "license": "MIT", - "dependencies": { - "@monaco-editor/loader": "^1.5.0" - }, - "peerDependencies": { - "monaco-editor": ">= 0.25.0 < 1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", - "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.11.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", - "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", - "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/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", - "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.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.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.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.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 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "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/@vitejs/plugin-react": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", - "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "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==", - "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==", - "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" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "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/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/blockly": { - "version": "12.4.1", - "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.4.1.tgz", - "integrity": "sha512-OEF0r8cFMGDkQbX+PWTjifWTe9xi2QzpZS4rO2lYeQhZDWW3/eInklLSdoxAyEQCdfJhQqMTpBct13oDoc0GVQ==", - "license": "Apache-2.0", - "dependencies": { - "jsdom": "26.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001775", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", - "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "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" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "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.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "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.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "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/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "license": "MIT", - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "license": "MIT", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "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/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", - "license": "MIT", - "peer": true, - "dependencies": { - "dompurify": "3.2.7", - "marked": "14.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "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", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/state-local": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", - "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", - "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.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 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "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" - }, - "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_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "license": "MIT" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/frontend-scratch-v2/package.json b/frontend-scratch-v2/package.json deleted file mode 100644 index 0d93cf3..0000000 --- a/frontend-scratch-v2/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "frontend-scratch-v2", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "test": "vitest run", - "preview": "vite preview" - }, - "dependencies": { - "@monaco-editor/react": "^4.7.0", - "ajv": "^8.18.0", - "blockly": "^12.4.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "yaml": "^2.8.2", - "zod": "^4.3.6" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.1", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.48.0", - "vite": "^7.3.1", - "vitest": "^3.2.4" - } -} diff --git a/frontend-scratch-v2/public/vite.svg b/frontend-scratch-v2/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend-scratch-v2/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend-scratch-v2/src/App.css b/frontend-scratch-v2/src/App.css deleted file mode 100644 index 2b24f89..0000000 --- a/frontend-scratch-v2/src/App.css +++ /dev/null @@ -1,511 +0,0 @@ -/* ─── Reset & Base ─── */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -:root { - --bg: #0f1117; - --bg-card: #1a1d27; - --bg-input: #252833; - --border: #2e3142; - --text: #e4e6eb; - --text-muted: #8b8fa3; - --accent: #3b82f6; - --accent-hover: #2563eb; - --green: #22c55e; - --red: #ef4444; - --orange: #f59e0b; - --radius: 8px; - --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - --mono: 'JetBrains Mono', 'Fira Code', monospace; -} - -body { - background: var(--bg); - color: var(--text); - font-family: var(--font); - font-size: 14px; - line-height: 1.5; -} - -/* ─── Shell ─── */ -.app-shell { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -/* ─── Header ─── */ -.app-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 20px; - background: var(--bg-card); - border-bottom: 1px solid var(--border); - gap: 16px; - flex-wrap: wrap; -} - -.app-header h1 { - font-size: 18px; - font-weight: 700; - white-space: nowrap; -} - -.header-status { - display: flex; - align-items: center; - gap: 10px; -} - -.dot { - width: 10px; - height: 10px; - border-radius: 50%; - flex-shrink: 0; -} -.dot.connected { - background: var(--green); - box-shadow: 0 0 6px var(--green); -} -.dot.disconnected { - background: var(--red); -} - -.api-url-input { - background: var(--bg-input); - border: 1px solid var(--border); - color: var(--text); - border-radius: var(--radius); - padding: 4px 10px; - font-size: 13px; - font-family: var(--mono); - width: 240px; -} - -.header-step { - color: var(--accent); - font-size: 13px; -} - -/* ─── Tabs ─── */ -.tab-bar { - display: flex; - gap: 0; - background: var(--bg-card); - border-bottom: 2px solid var(--border); - padding: 0 20px; -} - -.tab { - padding: 10px 20px; - background: none; - border: none; - color: var(--text-muted); - font-size: 14px; - font-weight: 500; - cursor: pointer; - border-bottom: 2px solid transparent; - margin-bottom: -2px; - transition: all 0.15s; - border-radius: 0; -} -.tab:hover { - color: var(--text); - background: none; - border-color: transparent; - border-bottom-color: var(--text-muted); -} -.tab.active { - color: var(--accent); - border-bottom-color: var(--accent); -} - -/* ─── Main ─── */ -.main-content { - flex: 1; - padding: 20px; - overflow-y: auto; -} - -/* ─── Cards ─── */ -.card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px; - margin-bottom: 16px; -} -.card h3 { - font-size: 14px; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 12px; -} - -.panel-stack { - display: flex; - flex-direction: column; - gap: 16px; -} - -.editor-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -/* ─── KV pairs ─── */ -.kv { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 0; - border-bottom: 1px solid var(--border); -} -.kv:last-child { - border-bottom: none; -} - -.runtime3-grid { - display: grid; - gap: 8px 16px; -} - -.runtime3-span { - grid-column: 1 / -1; -} - -.runtime3-path { - word-break: break-all; - text-align: right; -} - -/* ─── Badges ─── */ -.badge { - display: inline-block; - padding: 2px 10px; - border-radius: 12px; - font-size: 12px; - font-weight: 600; - background: var(--bg-input); - color: var(--text-muted); -} -.badge.ok { - background: rgba(34, 197, 94, 0.15); - color: var(--green); -} -.badge.err { - background: rgba(239, 68, 68, 0.15); - color: var(--red); -} -.badge.active { - background: rgba(59, 130, 246, 0.15); - color: var(--accent); -} - -/* ─── Buttons ─── */ -button { - padding: 6px 14px; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-input); - color: var(--text); - font-size: 13px; - cursor: pointer; - transition: all 0.15s; -} -button:hover:not(:disabled) { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} -button:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.actions { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-top: 8px; -} - -/* ─── Inputs ─── */ -input[type='text'], -input[type='number'] { - background: var(--bg-input); - border: 1px solid var(--border); - color: var(--text); - border-radius: var(--radius); - padding: 6px 10px; - font-size: 13px; -} - -label { - display: flex; - flex-direction: column; - gap: 4px; - font-size: 12px; - color: var(--text-muted); -} - -/* ─── Notices ─── */ -.notice { - padding: 8px 12px; - border-radius: var(--radius); - font-size: 13px; - margin: 8px 0; -} -.notice.warn { - background: rgba(245, 158, 11, 0.12); - color: var(--orange); - border: 1px solid rgba(245, 158, 11, 0.3); -} -.notice.info { - background: rgba(59, 130, 246, 0.12); - color: var(--accent); - border: 1px solid rgba(59, 130, 246, 0.3); -} -.notice.err { - background: rgba(239, 68, 68, 0.12); - color: var(--red); - border: 1px solid rgba(239, 68, 68, 0.3); -} - -/* ─── Progress ─── */ -.progress-bar { - display: flex; - align-items: center; - gap: 8px; - flex: 1; -} -.progress-bar .progress-fill { - height: 6px; - background: var(--accent); - border-radius: 3px; - transition: width 0.3s; - flex: 1; - max-width: 200px; -} -.progress-bar span { - font-size: 12px; - font-family: var(--mono); -} - -/* ─── Lists ─── */ -.scenario-list, -.file-list { - list-style: none; -} -.scenario-list li, -.file-list li { - display: flex; - align-items: center; - gap: 10px; - padding: 6px 0; - border-bottom: 1px solid var(--border); -} -.scenario-list li:last-child, -.file-list li:last-child { - border-bottom: none; -} - -/* ─── Misc ─── */ -.mono { - font-family: var(--mono); - font-size: 13px; -} -.muted { - color: var(--text-muted); - font-size: 13px; -} -.feedback { - margin-top: 8px; - padding: 6px 10px; - border-radius: var(--radius); - background: rgba(59, 130, 246, 0.1); - color: var(--accent); - font-size: 13px; -} -.error-banner { - background: rgba(239, 68, 68, 0.12); - color: var(--red); - padding: 8px 12px; - border-radius: var(--radius); - margin-top: 12px; -} -.event-json { - font-family: var(--mono); - font-size: 12px; - color: var(--text-muted); - background: var(--bg); - padding: 8px; - border-radius: var(--radius); - overflow-x: auto; - max-height: 200px; -} - -/* ─── Designer grid ─── */ -.app-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; -} -@media (max-width: 900px) { - .app-grid { - grid-template-columns: 1fr; - } -} - -.panel { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px; -} -.panel h2 { - font-size: 15px; - font-weight: 600; - margin-bottom: 12px; -} - -/* ─── Blockly ─── */ -.blockly-host { - height: 50vh; - min-height: 340px; - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; -} - -.toolbar { - display: flex; - gap: 8px; - margin-bottom: 8px; - flex-wrap: wrap; - align-items: center; -} - -.status-line { - margin-top: 8px; - padding: 6px 10px; - border-radius: var(--radius); - background: rgba(34, 197, 94, 0.1); - color: var(--green); - font-size: 13px; -} -.status-line.error { - background: rgba(239, 68, 68, 0.1); - color: var(--red); -} - -/* ─── Runtime panel ─── */ -.runtime-panel { - margin-top: 12px; -} -.runtime-panel pre { - font-family: var(--mono); - font-size: 12px; - color: var(--text-muted); - background: var(--bg); - padding: 8px; - border-radius: var(--radius); - overflow-x: auto; - max-height: 200px; - margin-top: 8px; - white-space: pre-wrap; -} - -/* ─── Rec form ─── */ -.rec-form { - display: flex; - gap: 12px; - margin-bottom: 8px; -} -.rec-form input { - width: 120px; -} - -/* ─── Dashboard layout ─── */ -.dashboard { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - gap: 16px; -} -.dashboard h2 { - grid-column: 1 / -1; - font-size: 18px; - font-weight: 700; -} - -.media-manager, -.network-panel { - max-width: 720px; -} - -/* ─── Scenario Editor ─── */ -.scenario-editor { - display: flex; - flex-direction: column; - height: calc(100vh - 120px); -} - -.scenario-editor-toolbar { - display: flex; - gap: 8px; - margin-bottom: 8px; - flex-wrap: wrap; - align-items: center; -} - -.scenario-editor-toolbar select { - background: var(--bg-input); - border: 1px solid var(--border); - color: var(--text); - border-radius: var(--radius); - padding: 6px 10px; - font-size: 13px; -} - -.scenario-editor-status { - color: var(--text-muted); - font-size: 13px; - margin-left: auto; -} - -.scenario-editor-split { - display: grid; - grid-template-columns: 3fr 2fr; - gap: 12px; - flex: 1; - min-height: 0; -} - -.scenario-editor-blockly { - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; - min-height: 400px; -} - -.scenario-editor-preview { - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; - min-height: 400px; -} - -@media (max-width: 900px) { - .scenario-editor-split { - grid-template-columns: 1fr; - } -} diff --git a/frontend-scratch-v2/src/App.tsx b/frontend-scratch-v2/src/App.tsx deleted file mode 100644 index 84ba32e..0000000 --- a/frontend-scratch-v2/src/App.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { Component, Suspense, lazy, useState } from 'react'; -import type { ErrorInfo, ReactNode } from 'react'; -import { RuntimeControls } from './components/RuntimeControls'; -import { Dashboard } from './components/Dashboard'; -import { useRuntimeStore } from './lib/useRuntimeStore'; -import { setApiBase, getApiBase } from './lib/api'; -import './App.css'; - -// ─── Lazy-loaded heavy components ─── - -const LazyBlocklyDesigner = lazy(() => - import('./components/BlocklyDesigner').then((m) => ({ default: m.BlocklyDesigner })), -); -const LazyMonacoEditor = lazy(() => import('@monaco-editor/react')); -const LazyMediaManager = lazy(() => - import('./components/MediaManager').then((m) => ({ default: m.MediaManager })), -); -const LazyNetworkPanel = lazy(() => - import('./components/NetworkPanel').then((m) => ({ default: m.NetworkPanel })), -); -const LazyScenarioEditor = lazy(() => - import('./components/ScenarioEditor').then((m) => ({ default: m.ScenarioEditor })), -); - -const LazyFallback = ( -
Loading editor...
-); - -// ─── ErrorBoundary ─── - -interface ErrorBoundaryProps { - children: ReactNode; - fallbackLabel?: string; -} - -interface ErrorBoundaryState { - hasError: boolean; - error: Error | null; -} - -class ErrorBoundary extends Component { - constructor(props: ErrorBoundaryProps) { - super(props); - this.state = { hasError: false, error: null }; - } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { hasError: true, error }; - } - - componentDidCatch(error: Error, info: ErrorInfo) { - console.error(`[ErrorBoundary${this.props.fallbackLabel ? ` ${this.props.fallbackLabel}` : ''}]`, error, info.componentStack); - } - - render() { - if (this.state.hasError) { - return ( -
-

Something went wrong{this.props.fallbackLabel ? ` in ${this.props.fallbackLabel}` : ''}.

-
{this.state.error?.message}
- -
- ); - } - return this.props.children; - } -} - -export { ErrorBoundary }; - -type Tab = 'dashboard' | 'designer' | 'scenario' | 'media' | 'network'; - -const TABS: { id: Tab; label: string }[] = [ - { id: 'dashboard', label: 'Dashboard' }, - { id: 'designer', label: 'Designer' }, - { id: 'scenario', label: 'Scenario' }, - { id: 'media', label: 'Media' }, - { id: 'network', label: 'Network' }, -]; - -function App() { - const [tab, setTab] = useState('dashboard'); - const [yaml, setYaml] = useState(''); - const [runtime3Json, setRuntime3Json] = useState(''); - const [apiUrl, setApiUrl] = useState(getApiBase()); - const runtime = useRuntimeStore(); - - const handleApiChange = (url: string) => { - setApiUrl(url); - setApiBase(url); - }; - - return ( -
-
-

Zacus Studio V2

-
- - handleApiChange(e.target.value.trim())} - aria-label="ESP32 API URL" - placeholder="http://esp32-ip:8080" - /> - {runtime.story && ( - - {runtime.story.current_step} - - )} -
-
- - - -
- {tab === 'dashboard' && ( - - - - )} - - {tab === 'designer' && ( - - -
-
-

Designer blocs

- { - setYaml(draft.yaml); - setRuntime3Json(draft.runtime3Json); - }} - /> -
-
-
-

YAML canonique

- -
-
-

IR Runtime 3

- -
- -
-
-
-
- )} - - {tab === 'scenario' && ( - - - - - - )} - - {tab === 'media' && ( - - - - - - )} - - {tab === 'network' && ( - - - - - - )} -
-
- ); -} - -export default App; diff --git a/frontend-scratch-v2/src/__tests__/api.test.ts b/frontend-scratch-v2/src/__tests__/api.test.ts deleted file mode 100644 index fc29116..0000000 --- a/frontend-scratch-v2/src/__tests__/api.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; - -// We need to test the internal jsonFetch + the exported helpers. -// Since jsonFetch is not exported, we test it indirectly through storyList / storyStatus -// and also test setApiBase / getApiBase directly. - -import { - setApiBase, - getApiBase, - storyList, - storyStatus, - ApiError, -} from '../lib/api'; - -// ─── Mock globalThis.fetch ─── - -const mockFetch = vi.fn<(...args: unknown[]) => Promise>(); - -beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - setApiBase('http://test-esp:8080'); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); - -// ─── Helpers ─── - -function jsonResponse(body: unknown, status = 200): Response { - return new Response(JSON.stringify(body), { - status, - headers: { 'Content-Type': 'application/json' }, - }); -} - -// ─── Tests ─── - -describe('setApiBase / getApiBase', () => { - it('stores and returns the base URL', () => { - setApiBase('http://192.168.0.42:8080'); - expect(getApiBase()).toBe('http://192.168.0.42:8080'); - }); - - it('strips trailing slashes', () => { - setApiBase('http://host:9000///'); - expect(getApiBase()).toBe('http://host:9000'); - }); -}); - -describe('jsonFetch (via storyList)', () => { - it('returns parsed JSON on 200', async () => { - const payload = { scenarios: [{ id: 'demo', version: 1, estimated_duration_s: 300 }] }; - mockFetch.mockResolvedValueOnce(jsonResponse(payload)); - - const result = await storyList(); - expect(result).toEqual(payload.scenarios); - expect(mockFetch).toHaveBeenCalledOnce(); - - const [url] = mockFetch.mock.calls[0] as [string]; - expect(url).toBe('http://test-esp:8080/api/story/list'); - }); - - it('throws ApiError on HTTP 500', async () => { - mockFetch.mockResolvedValueOnce( - jsonResponse({ error: 'internal failure' }, 500), - ); - - try { - await storyList(); - expect.unreachable('should have thrown'); - } catch (err) { - expect(err).toBeInstanceOf(ApiError); - expect((err as ApiError).status).toBe(500); - expect((err as ApiError).message).toBe('internal failure'); - } - }); - - it('throws on timeout (AbortError path)', async () => { - // Simulate a request that never resolves — the AbortController will fire - mockFetch.mockImplementation( - (...args: unknown[]) => - new Promise((_resolve, reject) => { - const init = args[1] as { signal?: AbortSignal } | undefined; - if (init?.signal) { - init.signal.addEventListener('abort', () => { - const err = new DOMException('The operation was aborted.', 'AbortError'); - reject(err); - }); - } - }), - ); - - // storyStatus uses the default 5000ms timeout — too long for tests. - // Instead we test indirectly: the AbortController fires, the promise rejects. - // We reduce the wait by calling storyStatus which has default timeout. - // For speed we rely on the abort signal being set up correctly. - const promise = storyStatus(); - await expect(promise).rejects.toThrow(/timed out/); - }, 10_000); -}); - -describe('storyList', () => { - it('calls /api/story/list and unwraps scenarios', async () => { - const scenarios = [ - { id: 'a', version: 2, estimated_duration_s: 600 }, - { id: 'b', version: 1, estimated_duration_s: 120 }, - ]; - mockFetch.mockResolvedValueOnce(jsonResponse({ scenarios })); - - const result = await storyList(); - expect(result).toHaveLength(2); - expect(result[0].id).toBe('a'); - }); -}); - -describe('storyStatus', () => { - it('calls /api/story/status and returns status object', async () => { - const status = { - status: 'running', - scenario_id: 'demo', - current_step: 'STEP_1', - progress_pct: 42, - started_at_ms: 1700000000000, - selected: 'demo', - queue_depth: 0, - }; - mockFetch.mockResolvedValueOnce(jsonResponse(status)); - - const result = await storyStatus(); - expect(result.status).toBe('running'); - expect(result.current_step).toBe('STEP_1'); - - const [url] = mockFetch.mock.calls[0] as [string]; - expect(url).toBe('http://test-esp:8080/api/story/status'); - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/runtime3.test.ts b/frontend-scratch-v2/src/__tests__/runtime3.test.ts deleted file mode 100644 index 7117ae8..0000000 --- a/frontend-scratch-v2/src/__tests__/runtime3.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - compileScenarioDocumentToRuntime3, - validateRuntime3Document, -} from '../lib/runtime3'; -import type { ScenarioDocument } from '../types'; - -// ─── Helpers ─── - -function makeDocument(overrides?: Partial): ScenarioDocument { - return { - id: 'test-scenario', - version: 1, - title: 'Test Scenario', - players: { min: 2, max: 6 }, - duration: { total_minutes: 45 }, - canon: { introduction: 'Intro', stakes: 'Stakes' }, - stations: [], - puzzles: [], - steps_narrative: [{ step_id: 'intro', scene: 'scene1', narrative: 'Start' }], - firmware: { - initial_step: 'intro', - steps: [ - { - step_id: 'intro', - screen_scene_id: 'scene_welcome', - audio_pack_id: 'pack_intro', - actions: ['play_music'], - apps: [], - transitions: [ - { - event_type: 'button', - event_name: 'btn_next', - target_step_id: 'puzzle_1', - priority: 10, - after_ms: 0, - }, - { - event_type: 'timer', - event_name: 'timeout', - target_step_id: 'puzzle_1', - priority: 5, - after_ms: 30000, - }, - ], - }, - { - step_id: 'puzzle_1', - screen_scene_id: 'scene_puzzle', - audio_pack_id: 'pack_puzzle', - actions: [], - apps: ['puzzle_app'], - transitions: [ - { - event_type: 'serial', - event_name: 'solved', - target_step_id: 'intro', - priority: 0, - after_ms: 0, - }, - ], - }, - ], - }, - ...overrides, - }; -} - -// ─── Tests ─── - -describe('compileScenarioDocumentToRuntime3', () => { - it('produces correct schema_version', () => { - const result = compileScenarioDocumentToRuntime3(makeDocument()); - expect(result.schema_version).toBe('zacus.runtime3.v1'); - }); - - it('sets metadata correctly', () => { - const result = compileScenarioDocumentToRuntime3(makeDocument()); - expect(result.metadata.generated_by).toBe('frontend-scratch-v2'); - expect(result.metadata.migration_mode).toBe('native'); - }); - - it('normalizes step IDs to uppercase', () => { - const result = compileScenarioDocumentToRuntime3(makeDocument()); - for (const step of result.steps) { - expect(step.id).toBe(step.id.toUpperCase()); - } - expect(result.steps[0].id).toBe('INTRO'); - expect(result.steps[1].id).toBe('PUZZLE_1'); - }); - - it('normalizes transition target_step_id to uppercase', () => { - const result = compileScenarioDocumentToRuntime3(makeDocument()); - for (const step of result.steps) { - for (const t of step.transitions) { - expect(t.target_step_id).toBe(t.target_step_id.toUpperCase()); - } - } - }); - - it('sorts transitions are preserved with priority values', () => { - const result = compileScenarioDocumentToRuntime3(makeDocument()); - const introTransitions = result.steps[0].transitions; - expect(introTransitions).toHaveLength(2); - // Priorities are preserved as-is from the source - expect(introTransitions[0].priority).toBe(10); - expect(introTransitions[1].priority).toBe(5); - }); - - it('sets entry_step_id correctly from firmware.initial_step', () => { - const result = compileScenarioDocumentToRuntime3(makeDocument()); - expect(result.scenario.entry_step_id).toBe('INTRO'); - }); - - it('falls back to first firmware step if initial_step is missing', () => { - const doc = makeDocument(); - delete doc.firmware!.initial_step; - const result = compileScenarioDocumentToRuntime3(doc); - expect(result.scenario.entry_step_id).toBe('INTRO'); - }); - - it('falls back to STEP_BOOT when no steps exist', () => { - const doc = makeDocument({ - firmware: { steps: [] }, - steps_narrative: [], - }); - const result = compileScenarioDocumentToRuntime3(doc); - expect(result.scenario.entry_step_id).toBe('STEP_BOOT'); - }); - - it('normalizes scenario id', () => { - const doc = makeDocument({ id: 'my cool scenario!' }); - const result = compileScenarioDocumentToRuntime3(doc); - expect(result.scenario.id).toBe('MY_COOL_SCENARIO'); - }); - - it('handles unknown event_type by falling back to serial', () => { - const doc = makeDocument(); - doc.firmware!.steps![0].transitions[0].event_type = 'unknown_event'; - const result = compileScenarioDocumentToRuntime3(doc); - expect(result.steps[0].transitions[0].event_type).toBe('serial'); - }); -}); - -describe('validateRuntime3Document', () => { - it('validates a correct document', () => { - const doc = compileScenarioDocumentToRuntime3(makeDocument()); - const result = validateRuntime3Document(doc); - expect(result).toEqual({ ok: true }); - }); - - it('rejects empty steps', () => { - const doc = compileScenarioDocumentToRuntime3( - makeDocument({ firmware: { steps: [] }, steps_narrative: [] }), - ); - doc.steps = []; - const result = validateRuntime3Document(doc); - expect(result).toEqual({ ok: false, error: 'runtime3 requires at least one step' }); - }); - - it('rejects duplicate step IDs', () => { - const doc = compileScenarioDocumentToRuntime3(makeDocument()); - doc.steps.push({ ...doc.steps[0] }); // duplicate - const result = validateRuntime3Document(doc); - expect(result.ok).toBe(false); - expect('error' in result && result.error).toContain('duplicate'); - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/scenario-editor/export.test.ts b/frontend-scratch-v2/src/__tests__/scenario-editor/export.test.ts deleted file mode 100644 index ab47745..0000000 --- a/frontend-scratch-v2/src/__tests__/scenario-editor/export.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { decodeScenarioFromUrl } from '../../components/ScenarioEditor/export/share'; - -// ─── Share URL encode/decode tests ─── -// We test the core encode/decode logic without requiring window.location. -// encodeScenarioToUrl() uses window.location at runtime, so we test the -// underlying btoa/atob round-trip directly via decodeScenarioFromUrl. - -function encodeForTest(xml: string): string { - const compressed = btoa(encodeURIComponent(xml)); - return `#scenario=${compressed}`; -} - -describe('share URL encode/decode round-trip', () => { - it('round-trips a simple XML string', () => { - const xml = ''; - const hash = encodeForTest(xml); - - expect(hash).toContain('#scenario='); - const decoded = decodeScenarioFromUrl(hash); - expect(decoded).toBe(xml); - }); - - it('round-trips XML with special characters (French, entities)', () => { - const xml = ''; - const hash = encodeForTest(xml); - const decoded = decodeScenarioFromUrl(hash); - expect(decoded).toBe(xml); - }); - - it('round-trips an empty workspace', () => { - const xml = ''; - const hash = encodeForTest(xml); - const decoded = decodeScenarioFromUrl(hash); - expect(decoded).toBe(xml); - }); - - it('returns null for invalid hash formats', () => { - expect(decodeScenarioFromUrl('')).toBeNull(); - expect(decodeScenarioFromUrl('#other=abc')).toBeNull(); - expect(decodeScenarioFromUrl('#noscenario')).toBeNull(); - expect(decodeScenarioFromUrl('no-hash-at-all')).toBeNull(); - }); - - it('returns null for corrupted base64 payload', () => { - expect(decodeScenarioFromUrl('#scenario=!!!invalid-base64!!!')).toBeNull(); - }); -}); - -// ─── Download utility tests ─── -// downloadYaml and downloadJson require document/URL which are browser-only. -// We verify the module exports exist and the function signatures are correct. -// Actual DOM interaction is covered by integration/E2E tests. - -describe('download module exports', () => { - it('exports downloadYaml function', async () => { - const mod = await import('../../components/ScenarioEditor/export/download'); - expect(typeof mod.downloadYaml).toBe('function'); - expect(mod.downloadYaml.length).toBe(2); // (yaml, filename) - }); - - it('exports downloadJson function', async () => { - const mod = await import('../../components/ScenarioEditor/export/download'); - expect(typeof mod.downloadJson).toBe('function'); - expect(mod.downloadJson.length).toBe(2); // (data, filename) - }); -}); - -describe('share module exports', () => { - it('exports encodeScenarioToUrl function', async () => { - const mod = await import('../../components/ScenarioEditor/export/share'); - expect(typeof mod.encodeScenarioToUrl).toBe('function'); - expect(mod.encodeScenarioToUrl.length).toBe(1); // (workspaceXml) - }); - - it('exports decodeScenarioFromUrl function', async () => { - const mod = await import('../../components/ScenarioEditor/export/share'); - expect(typeof mod.decodeScenarioFromUrl).toBe('function'); - expect(mod.decodeScenarioFromUrl.length).toBe(1); // (hash) - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/scenario-editor/hardware-deploy-blocks.test.ts b/frontend-scratch-v2/src/__tests__/scenario-editor/hardware-deploy-blocks.test.ts deleted file mode 100644 index f0e297e..0000000 --- a/frontend-scratch-v2/src/__tests__/scenario-editor/hardware-deploy-blocks.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import * as Blockly from 'blockly'; -import { ensureScenarioBlocks } from '../../components/ScenarioEditor/blocks/scene'; -import { registerPuzzleBlocks } from '../../components/ScenarioEditor/blocks/puzzle'; -import { registerNpcBlocks } from '../../components/ScenarioEditor/blocks/npc'; -import { registerHardwareBlocks } from '../../components/ScenarioEditor/blocks/hardware'; -import { registerDeployBlocks } from '../../components/ScenarioEditor/blocks/deploy'; -import { - buildScenarioGraph, - scenarioGraphToFirmwareYaml, - scenarioGraphToDisplayYaml, -} from '../../components/ScenarioEditor/generators/yaml'; -import YAML from 'yaml'; - -beforeAll(() => { - ensureScenarioBlocks(); - registerPuzzleBlocks(); - registerNpcBlocks(); - registerHardwareBlocks(); - registerDeployBlocks(); -}); - -function createWorkspace(): Blockly.Workspace { - return new Blockly.Workspace(); -} - -// ─── Hardware blocks ─── - -describe('hw_gpio_write block', () => { - it('creates a valid HardwareAction for gpio_write', () => { - const ws = createWorkspace(); - const block = ws.newBlock('hw_gpio_write'); - block.setFieldValue('12', 'PIN'); - block.setFieldValue('LOW', 'STATE'); - - const graph = buildScenarioGraph(ws); - const gpioActions = graph.hardwareActions.filter((a) => a.type === 'gpio_write'); - expect(gpioActions).toHaveLength(1); - expect(gpioActions[0].pin).toBe(12); - expect(gpioActions[0].state).toBe('LOW'); - - ws.dispose(); - }); -}); - -describe('hw_gpio_read block', () => { - it('creates a valid HardwareAction for gpio_read', () => { - const ws = createWorkspace(); - const block = ws.newBlock('hw_gpio_read'); - block.setFieldValue('7', 'PIN'); - block.setFieldValue('sensor_val', 'VARIABLE'); - - const graph = buildScenarioGraph(ws); - const readActions = graph.hardwareActions.filter((a) => a.type === 'gpio_read'); - expect(readActions).toHaveLength(1); - expect(readActions[0].pin).toBe(7); - expect(readActions[0].variable).toBe('sensor_val'); - - ws.dispose(); - }); -}); - -describe('hw_led_set block', () => { - it('creates HardwareAction with color and animation', () => { - const ws = createWorkspace(); - const block = ws.newBlock('hw_led_set'); - block.setFieldValue('#FF0000', 'COLOR'); - block.setFieldValue('blink', 'ANIMATION'); - - const graph = buildScenarioGraph(ws); - const ledActions = graph.hardwareActions.filter((a) => a.type === 'led_set'); - expect(ledActions).toHaveLength(1); - expect(ledActions[0].color).toBe('#FF0000'); - expect(ledActions[0].animation).toBe('blink'); - - ws.dispose(); - }); -}); - -describe('hw_buzzer_tone block', () => { - it('creates HardwareAction with frequency and duration', () => { - const ws = createWorkspace(); - const block = ws.newBlock('hw_buzzer_tone'); - block.setFieldValue('880', 'FREQUENCY'); - block.setFieldValue('1000', 'DURATION_MS'); - - const graph = buildScenarioGraph(ws); - const buzzerActions = graph.hardwareActions.filter((a) => a.type === 'buzzer'); - expect(buzzerActions).toHaveLength(1); - expect(buzzerActions[0].frequency).toBe(880); - expect(buzzerActions[0].duration_ms).toBe(1000); - - ws.dispose(); - }); -}); - -describe('hw_play_audio block', () => { - it('creates HardwareAction with filename', () => { - const ws = createWorkspace(); - const block = ws.newBlock('hw_play_audio'); - block.setFieldValue('intro.mp3', 'FILENAME'); - - const graph = buildScenarioGraph(ws); - const audioActions = graph.hardwareActions.filter((a) => a.type === 'play_audio'); - expect(audioActions).toHaveLength(1); - expect(audioActions[0].filename).toBe('intro.mp3'); - - ws.dispose(); - }); -}); - -describe('hw_qr_scan block', () => { - it('creates HardwareAction for qr_scan with no extra fields', () => { - const ws = createWorkspace(); - ws.newBlock('hw_qr_scan'); - - const graph = buildScenarioGraph(ws); - const qrActions = graph.hardwareActions.filter((a) => a.type === 'qr_scan'); - expect(qrActions).toHaveLength(1); - - ws.dispose(); - }); -}); - -// ─── Deploy blocks ─── - -describe('deploy_config_wifi block', () => { - it('produces DeployConfig with wifi settings', () => { - const ws = createWorkspace(); - const block = ws.newBlock('deploy_config_wifi'); - block.setFieldValue('ZacusNet', 'SSID'); - block.setFieldValue('s3cr3t', 'PASSWORD'); - - const graph = buildScenarioGraph(ws); - expect(graph.deploy.wifi).toBeDefined(); - expect(graph.deploy.wifi!.ssid).toBe('ZacusNet'); - expect(graph.deploy.wifi!.password).toBe('s3cr3t'); - - ws.dispose(); - }); -}); - -describe('deploy_config_tts block', () => { - it('produces DeployConfig with TTS settings', () => { - const ws = createWorkspace(); - const block = ws.newBlock('deploy_config_tts'); - block.setFieldValue('http://tower:8001', 'URL'); - block.setFieldValue('siwis', 'VOICE'); - - const graph = buildScenarioGraph(ws); - expect(graph.deploy.tts).toBeDefined(); - expect(graph.deploy.tts!.url).toBe('http://tower:8001'); - expect(graph.deploy.tts!.voice).toBe('siwis'); - - ws.dispose(); - }); -}); - -describe('deploy_config_llm block', () => { - it('produces DeployConfig with LLM settings', () => { - const ws = createWorkspace(); - const block = ws.newBlock('deploy_config_llm'); - block.setFieldValue('http://localhost:11434', 'URL'); - block.setFieldValue('qwen2.5', 'MODEL'); - - const graph = buildScenarioGraph(ws); - expect(graph.deploy.llm).toBeDefined(); - expect(graph.deploy.llm!.url).toBe('http://localhost:11434'); - expect(graph.deploy.llm!.model).toBe('qwen2.5'); - - ws.dispose(); - }); -}); - -describe('deploy_export block', () => { - it('can be created without errors', () => { - const ws = createWorkspace(); - const block = ws.newBlock('deploy_export'); - expect(block.type).toBe('deploy_export'); - ws.dispose(); - }); -}); - -// ─── Full scenario integration ─── - -describe('full scenario with hardware + deploy generates complete YAML', () => { - it('produces display YAML with hardware and deploy sections', () => { - const ws = createWorkspace(); - - // Scene - const scene = ws.newBlock('scenario_scene'); - scene.setFieldValue('SCENE_LAB', 'NAME'); - scene.setFieldValue('Laboratory', 'DESCRIPTION'); - scene.setFieldValue('180', 'DURATION_MAX'); - - // Hardware: LED + buzzer - const led = ws.newBlock('hw_led_set'); - led.setFieldValue('#0000FF', 'COLOR'); - led.setFieldValue('pulse', 'ANIMATION'); - - const buzzer = ws.newBlock('hw_buzzer_tone'); - buzzer.setFieldValue('660', 'FREQUENCY'); - buzzer.setFieldValue('250', 'DURATION_MS'); - - // Deploy: WiFi + TTS - const wifi = ws.newBlock('deploy_config_wifi'); - wifi.setFieldValue('EscapeRoom', 'SSID'); - wifi.setFieldValue('pass123', 'PASSWORD'); - - const tts = ws.newBlock('deploy_config_tts'); - tts.setFieldValue('http://192.168.0.120:8001', 'URL'); - tts.setFieldValue('tom-medium', 'VOICE'); - - const graph = buildScenarioGraph(ws); - expect(graph.scenes).toHaveLength(1); - expect(graph.hardwareActions).toHaveLength(2); - expect(graph.deploy.wifi).toBeDefined(); - expect(graph.deploy.tts).toBeDefined(); - - // Display YAML - const displayYaml = scenarioGraphToDisplayYaml(graph); - const parsed = YAML.parse(displayYaml); - expect(parsed.scenes).toHaveLength(1); - expect(parsed.scenes[0].name).toBe('SCENE_LAB'); - expect(parsed.hardware).toHaveLength(2); - expect(parsed.hardware[0].type).toBe('led_set'); - expect(parsed.hardware[0].color).toBe('#0000FF'); - expect(parsed.hardware[1].type).toBe('buzzer'); - expect(parsed.deploy).toBeDefined(); - expect(parsed.deploy.wifi.ssid).toBe('EscapeRoom'); - expect(parsed.deploy.tts.voice).toBe('tom-medium'); - - // Firmware YAML - const firmwareYaml = scenarioGraphToFirmwareYaml(graph); - const fwParsed = YAML.parse(firmwareYaml); - expect(fwParsed.firmware.steps).toHaveLength(1); - expect(fwParsed.firmware.steps[0].actions).toHaveLength(2); - expect(fwParsed.firmware.steps[0].actions[0].type).toBe('led_set'); - expect(fwParsed.firmware.deploy).toBeDefined(); - expect(fwParsed.firmware.deploy.wifi.ssid).toBe('EscapeRoom'); - - ws.dispose(); - }); - - it('produces clean YAML when no hardware or deploy blocks are present', () => { - const ws = createWorkspace(); - - const scene = ws.newBlock('scenario_scene'); - scene.setFieldValue('SCENE_EMPTY', 'NAME'); - - const graph = buildScenarioGraph(ws); - expect(graph.hardwareActions).toHaveLength(0); - expect(graph.deploy).toEqual({}); - - const displayYaml = scenarioGraphToDisplayYaml(graph); - const parsed = YAML.parse(displayYaml); - expect(parsed.hardware).toBeUndefined(); - expect(parsed.deploy).toBeUndefined(); - - ws.dispose(); - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/scenario-editor/puzzle-npc-blocks.test.ts b/frontend-scratch-v2/src/__tests__/scenario-editor/puzzle-npc-blocks.test.ts deleted file mode 100644 index 8211523..0000000 --- a/frontend-scratch-v2/src/__tests__/scenario-editor/puzzle-npc-blocks.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import * as Blockly from 'blockly'; -import { ensureScenarioBlocks } from '../../components/ScenarioEditor/blocks/scene'; -import { registerPuzzleBlocks } from '../../components/ScenarioEditor/blocks/puzzle'; -import { registerNpcBlocks } from '../../components/ScenarioEditor/blocks/npc'; -import { - buildScenarioGraph, - scenarioGraphToFirmwareYaml, - scenarioGraphToDisplayYaml, -} from '../../components/ScenarioEditor/generators/yaml'; -import YAML from 'yaml'; - -beforeAll(() => { - ensureScenarioBlocks(); - registerPuzzleBlocks(); - registerNpcBlocks(); -}); - -function createWorkspace(): Blockly.Workspace { - return new Blockly.Workspace(); -} - -describe('puzzle_definition block', () => { - it('creates a valid PuzzleNode from a puzzle block', () => { - const ws = createWorkspace(); - const block = ws.newBlock('puzzle_definition'); - block.setFieldValue('PUZZLE_QR_1', 'NAME'); - block.setFieldValue('qr', 'PUZZLE_TYPE'); - - const graph = buildScenarioGraph(ws); - expect(graph.puzzles).toHaveLength(1); - expect(graph.puzzles[0].name).toBe('PUZZLE_QR_1'); - expect(graph.puzzles[0].type).toBe('qr'); - expect(graph.puzzles[0].hints).toEqual([]); - - ws.dispose(); - }); - - it('reads hints nested inside puzzle_definition', () => { - const ws = createWorkspace(); - const puzzle = ws.newBlock('puzzle_definition'); - puzzle.setFieldValue('PUZZLE_DOOR', 'NAME'); - puzzle.setFieldValue('button', 'PUZZLE_TYPE'); - - const hint1 = ws.newBlock('npc_hint'); - hint1.setFieldValue('1', 'LEVEL'); - hint1.setFieldValue('Look at the door', 'TEXT'); - hint1.setFieldValue('PUZZLE_DOOR', 'PUZZLE_ID'); - - const hint2 = ws.newBlock('npc_hint'); - hint2.setFieldValue('2', 'LEVEL'); - hint2.setFieldValue('Try the red button', 'TEXT'); - hint2.setFieldValue('PUZZLE_DOOR', 'PUZZLE_ID'); - - // Connect hints to puzzle HINTS input - const hintsInput = puzzle.getInput('HINTS'); - if (hintsInput?.connection && hint1.previousConnection) { - hintsInput.connection.connect(hint1.previousConnection); - } - if (hint1.nextConnection && hint2.previousConnection) { - hint1.nextConnection.connect(hint2.previousConnection); - } - - const graph = buildScenarioGraph(ws); - expect(graph.puzzles).toHaveLength(1); - expect(graph.puzzles[0].hints).toHaveLength(2); - expect(graph.puzzles[0].hints[0].level).toBe(1); - expect(graph.puzzles[0].hints[0].text).toBe('Look at the door'); - expect(graph.puzzles[0].hints[1].level).toBe(2); - - ws.dispose(); - }); -}); - -describe('puzzle_validation_qr block', () => { - it('produces correct solution field when connected to puzzle', () => { - const ws = createWorkspace(); - const puzzle = ws.newBlock('puzzle_definition'); - puzzle.setFieldValue('QR_PUZZLE', 'NAME'); - puzzle.setFieldValue('qr', 'PUZZLE_TYPE'); - - const qrVal = ws.newBlock('puzzle_validation_qr'); - qrVal.setFieldValue('ZACUS_KEY_1', 'EXPECTED'); - - // Connect QR validation to SOLUTION input - const solutionInput = puzzle.getInput('SOLUTION'); - if (solutionInput?.connection && qrVal.outputConnection) { - solutionInput.connection.connect(qrVal.outputConnection); - } - - const graph = buildScenarioGraph(ws); - expect(graph.puzzles[0].solution).toBe('qr:ZACUS_KEY_1'); - - ws.dispose(); - }); -}); - -describe('puzzle_validation_button block', () => { - it('produces correct solution field for button validation', () => { - const ws = createWorkspace(); - const puzzle = ws.newBlock('puzzle_definition'); - puzzle.setFieldValue('BTN_PUZZLE', 'NAME'); - puzzle.setFieldValue('button', 'PUZZLE_TYPE'); - - const btnVal = ws.newBlock('puzzle_validation_button'); - btnVal.setFieldValue('7', 'PIN'); - - const solutionInput = puzzle.getInput('SOLUTION'); - if (solutionInput?.connection && btnVal.outputConnection) { - solutionInput.connection.connect(btnVal.outputConnection); - } - - const graph = buildScenarioGraph(ws); - expect(graph.puzzles[0].solution).toBe('button:7'); - - ws.dispose(); - }); -}); - -describe('puzzle_condition block', () => { - it('creates a condition with type and reference', () => { - const ws = createWorkspace(); - const cond = ws.newBlock('puzzle_condition'); - cond.setFieldValue('puzzle_solved', 'CONDITION_TYPE'); - cond.setFieldValue('PUZZLE_QR_1', 'REFERENCE'); - - // puzzle_condition is a value block (output), verify it exists - expect(cond.outputConnection).toBeTruthy(); - - ws.dispose(); - }); -}); - -describe('npc_say block', () => { - it('creates NPCAction with text and mood', () => { - const ws = createWorkspace(); - const say = ws.newBlock('npc_say'); - say.setFieldValue('Welcome, adventurer!', 'TEXT'); - say.setFieldValue('amused', 'MOOD'); - - const graph = buildScenarioGraph(ws); - const sayActions = graph.npcActions.filter((a) => a.type === 'say'); - expect(sayActions).toHaveLength(1); - expect(sayActions[0].text).toBe('Welcome, adventurer!'); - expect(sayActions[0].mood).toBe('amused'); - - ws.dispose(); - }); -}); - -describe('npc_hint block (standalone)', () => { - it('links hint to puzzle ID', () => { - const ws = createWorkspace(); - const hint = ws.newBlock('npc_hint'); - hint.setFieldValue('2', 'LEVEL'); - hint.setFieldValue('Check under the table', 'TEXT'); - hint.setFieldValue('PUZZLE_DOOR', 'PUZZLE_ID'); - - const graph = buildScenarioGraph(ws); - const hintActions = graph.npcActions.filter((a) => a.type === 'hint'); - expect(hintActions).toHaveLength(1); - expect(hintActions[0].level).toBe(2); - expect(hintActions[0].text).toBe('Check under the table'); - expect(hintActions[0].puzzleId).toBe('PUZZLE_DOOR'); - - ws.dispose(); - }); -}); - -describe('npc_react block', () => { - it('creates a react action with condition', () => { - const ws = createWorkspace(); - const react = ws.newBlock('npc_react'); - react.setFieldValue('Well done!', 'RESPONSE'); - - const cond = ws.newBlock('puzzle_condition'); - cond.setFieldValue('puzzle_solved', 'CONDITION_TYPE'); - cond.setFieldValue('QR_1', 'REFERENCE'); - - const condInput = react.getInput('CONDITION'); - if (condInput?.connection && cond.outputConnection) { - condInput.connection.connect(cond.outputConnection); - } - - const graph = buildScenarioGraph(ws); - const reactActions = graph.npcActions.filter((a) => a.type === 'react'); - expect(reactActions).toHaveLength(1); - expect(reactActions[0].text).toBe('Well done!'); - expect(reactActions[0].condition).toBe('puzzle_condition'); - - ws.dispose(); - }); -}); - -describe('npc_conversation block', () => { - it('creates a conversation action with system prompt', () => { - const ws = createWorkspace(); - const conv = ws.newBlock('npc_conversation'); - conv.setFieldValue('You are Professor Zacus', 'SYSTEM_PROMPT'); - conv.setFieldValue('escape room intro', 'CONTEXT'); - - const graph = buildScenarioGraph(ws); - const convActions = graph.npcActions.filter((a) => a.type === 'conversation'); - expect(convActions).toHaveLength(1); - expect(convActions[0].systemPrompt).toBe('You are Professor Zacus'); - expect(convActions[0].text).toBe('escape room intro'); - - ws.dispose(); - }); -}); - -describe('full scenario with scenes + puzzles + NPC generates valid YAML', () => { - it('produces display YAML with scenes, puzzles, and npc sections', () => { - const ws = createWorkspace(); - - // Scene - const scene = ws.newBlock('scenario_scene'); - scene.setFieldValue('SCENE_INTRO', 'NAME'); - scene.setFieldValue('Welcome', 'DESCRIPTION'); - scene.setFieldValue('120', 'DURATION_MAX'); - - // Puzzle - const puzzle = ws.newBlock('puzzle_definition'); - puzzle.setFieldValue('PUZZLE_QR', 'NAME'); - puzzle.setFieldValue('qr', 'PUZZLE_TYPE'); - - const qrVal = ws.newBlock('puzzle_validation_qr'); - qrVal.setFieldValue('KEY_42', 'EXPECTED'); - const solInput = puzzle.getInput('SOLUTION'); - if (solInput?.connection && qrVal.outputConnection) { - solInput.connection.connect(qrVal.outputConnection); - } - - // NPC say (standalone, not inside scene actions) - const say = ws.newBlock('npc_say'); - say.setFieldValue('Bienvenue!', 'TEXT'); - say.setFieldValue('impressed', 'MOOD'); - - const graph = buildScenarioGraph(ws); - expect(graph.scenes).toHaveLength(1); - expect(graph.puzzles).toHaveLength(1); - expect(graph.npcActions.length).toBeGreaterThanOrEqual(1); - - // Display YAML - const displayYaml = scenarioGraphToDisplayYaml(graph); - const parsed = YAML.parse(displayYaml); - expect(parsed.scenes).toHaveLength(1); - expect(parsed.scenes[0].name).toBe('SCENE_INTRO'); - expect(parsed.puzzles).toHaveLength(1); - expect(parsed.puzzles[0].name).toBe('PUZZLE_QR'); - expect(parsed.puzzles[0].type).toBe('qr'); - expect(parsed.puzzles[0].solution).toBe('qr:KEY_42'); - expect(parsed.npc).toBeDefined(); - expect(parsed.npc.length).toBeGreaterThanOrEqual(1); - - // Firmware YAML - const firmwareYaml = scenarioGraphToFirmwareYaml(graph); - const fwParsed = YAML.parse(firmwareYaml); - expect(fwParsed.firmware.steps).toHaveLength(1); - expect(fwParsed.firmware.puzzles).toHaveLength(1); - expect(fwParsed.firmware.puzzles[0].puzzle_id).toBe('PUZZLE_QR'); - expect(fwParsed.firmware.puzzles[0].type).toBe('qr'); - - ws.dispose(); - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/scenario-editor/scene-blocks.test.ts b/frontend-scratch-v2/src/__tests__/scenario-editor/scene-blocks.test.ts deleted file mode 100644 index da3f8ab..0000000 --- a/frontend-scratch-v2/src/__tests__/scenario-editor/scene-blocks.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import * as Blockly from 'blockly'; -import { ensureScenarioBlocks } from '../../components/ScenarioEditor/blocks/scene'; -import { - buildScenarioGraph, - scenarioGraphToFirmwareYaml, - scenarioGraphToDisplayYaml, -} from '../../components/ScenarioEditor/generators/yaml'; -import YAML from 'yaml'; - -beforeAll(() => { - ensureScenarioBlocks(); -}); - -function createWorkspace(): Blockly.Workspace { - return new Blockly.Workspace(); -} - -describe('scenario_scene block', () => { - it('creates a valid graph node from a single scene block', () => { - const ws = createWorkspace(); - const block = ws.newBlock('scenario_scene'); - block.setFieldValue('SCENE_INTRO', 'NAME'); - block.setFieldValue('Welcome to the game', 'DESCRIPTION'); - block.setFieldValue('120', 'DURATION_MAX'); - - const graph = buildScenarioGraph(ws); - expect(graph.scenes).toHaveLength(1); - expect(graph.scenes[0].name).toBe('SCENE_INTRO'); - expect(graph.scenes[0].description).toBe('Welcome to the game'); - expect(graph.scenes[0].durationMax).toBe(120); - expect(graph.scenes[0].transitions).toEqual([]); - expect(graph.scenes[0].actions).toEqual([]); - - ws.dispose(); - }); -}); - -describe('scenario_transition block', () => { - it('links two scenes via a transition', () => { - const ws = createWorkspace(); - - // Create scene with a transition in TRANSITIONS input - const scene = ws.newBlock('scenario_scene'); - scene.setFieldValue('SCENE_A', 'NAME'); - - const transition = ws.newBlock('scenario_transition'); - transition.setFieldValue('SCENE_B', 'TARGET_SCENE'); - - // Connect transition to scene's TRANSITIONS input - const input = scene.getInput('TRANSITIONS'); - if (input?.connection && transition.previousConnection) { - input.connection.connect(transition.previousConnection); - } - - const graph = buildScenarioGraph(ws); - expect(graph.scenes).toHaveLength(1); - expect(graph.scenes[0].transitions).toHaveLength(1); - expect(graph.scenes[0].transitions[0].targetScene).toBe('SCENE_B'); - - ws.dispose(); - }); -}); - -describe('scenario_timer block', () => { - it('produces correct firmware transition from timer action', () => { - const ws = createWorkspace(); - - const scene = ws.newBlock('scenario_scene'); - scene.setFieldValue('SCENE_TIMED', 'NAME'); - - const timer = ws.newBlock('scenario_timer'); - timer.setFieldValue('30', 'SECONDS'); - - // Add a variable_set inside timer's ON_EXPIRE - const varSet = ws.newBlock('scenario_variable_set'); - varSet.setFieldValue('game_over', 'NAME'); - - const timerOnExpire = timer.getInput('ON_EXPIRE'); - if (timerOnExpire?.connection && varSet.previousConnection) { - timerOnExpire.connection.connect(varSet.previousConnection); - } - - // Connect timer to scene's ACTIONS input - const actionsInput = scene.getInput('ACTIONS'); - if (actionsInput?.connection && timer.previousConnection) { - actionsInput.connection.connect(timer.previousConnection); - } - - const graph = buildScenarioGraph(ws); - expect(graph.scenes[0].actions).toHaveLength(1); - expect(graph.scenes[0].actions[0].kind).toBe('timer'); - - const timerAction = graph.scenes[0].actions[0]; - if (timerAction.kind === 'timer') { - expect(timerAction.seconds).toBe(30); - expect(timerAction.onExpire).toHaveLength(1); - expect(timerAction.onExpire[0].kind).toBe('variable_set'); - } - - // Check firmware YAML output - const firmwareYaml = scenarioGraphToFirmwareYaml(graph); - const parsed = YAML.parse(firmwareYaml); - const step = parsed.firmware.steps[0]; - expect(step.step_id).toBe('SCENE_TIMED'); - - // Timer with variable_set on_expire should produce an action-type transition - const actionTransition = step.transitions.find( - (t: { event_type: string }) => t.event_type === 'action', - ); - expect(actionTransition).toBeDefined(); - expect(actionTransition.after_ms).toBe(30000); - - ws.dispose(); - }); -}); - -describe('full graph to firmware YAML round-trip', () => { - it('generates valid firmware YAML from a multi-scene graph', () => { - const ws = createWorkspace(); - - // Scene A - const sceneA = ws.newBlock('scenario_scene'); - sceneA.setFieldValue('SCENE_A', 'NAME'); - sceneA.setFieldValue('First scene', 'DESCRIPTION'); - sceneA.setFieldValue('60', 'DURATION_MAX'); - - // Transition A -> B - const transAB = ws.newBlock('scenario_transition'); - transAB.setFieldValue('SCENE_B', 'TARGET_SCENE'); - const inputA = sceneA.getInput('TRANSITIONS'); - if (inputA?.connection && transAB.previousConnection) { - inputA.connection.connect(transAB.previousConnection); - } - - // Scene B - const sceneB = ws.newBlock('scenario_scene'); - sceneB.setFieldValue('SCENE_B', 'NAME'); - sceneB.setFieldValue('Second scene', 'DESCRIPTION'); - - const graph = buildScenarioGraph(ws); - expect(graph.scenes).toHaveLength(2); - - // Firmware YAML - const firmwareYaml = scenarioGraphToFirmwareYaml(graph); - const fwParsed = YAML.parse(firmwareYaml); - expect(fwParsed.firmware.initial_step).toBe('SCENE_A'); - expect(fwParsed.firmware.steps).toHaveLength(2); - expect(fwParsed.firmware.steps[0].step_id).toBe('SCENE_A'); - expect(fwParsed.firmware.steps[0].transitions[0].target_step_id).toBe('SCENE_B'); - expect(fwParsed.firmware.steps[1].step_id).toBe('SCENE_B'); - - // Display YAML - const displayYaml = scenarioGraphToDisplayYaml(graph); - const displayParsed = YAML.parse(displayYaml); - expect(displayParsed.scenes).toHaveLength(2); - expect(displayParsed.scenes[0].name).toBe('SCENE_A'); - expect(displayParsed.scenes[0].description).toBe('First scene'); - expect(displayParsed.scenes[0].duration_max_s).toBe(60); - expect(displayParsed.scenes[0].transitions[0].target).toBe('SCENE_B'); - expect(displayParsed.scenes[1].name).toBe('SCENE_B'); - - ws.dispose(); - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/scenario-editor/validation.test.ts b/frontend-scratch-v2/src/__tests__/scenario-editor/validation.test.ts deleted file mode 100644 index 552dd4c..0000000 --- a/frontend-scratch-v2/src/__tests__/scenario-editor/validation.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - validateScenarioGraph, - formatValidationSummary, -} from '../../components/ScenarioEditor/validators/workspace'; -import type { ScenarioGraph } from '../../components/ScenarioEditor/types'; - -function makeGraph(partial: Partial): ScenarioGraph { - return { - scenes: [], - puzzles: [], - npcActions: [], - hardwareActions: [], - deploy: {}, - ...partial, - }; -} - -describe('validateScenarioGraph', () => { - it('returns error for empty scenario', () => { - const graph = makeGraph({}); - const issues = validateScenarioGraph(graph); - expect(issues.some((i) => i.message === 'No scenes defined')).toBe(true); - }); - - it('detects duplicate scene IDs', () => { - const graph = makeGraph({ - scenes: [ - { name: 'SCENE_1', description: '', durationMax: 300, actions: [], transitions: [] }, - { name: 'SCENE_1', description: '', durationMax: 300, actions: [], transitions: [] }, - ], - }); - const issues = validateScenarioGraph(graph); - expect(issues.some((i) => i.message.includes('Duplicate scene ID'))).toBe(true); - }); - - it('detects dangling transition target', () => { - const graph = makeGraph({ - scenes: [ - { - name: 'SCENE_1', - description: '', - durationMax: 300, - actions: [], - transitions: [{ targetScene: 'NONEXISTENT', condition: '' }], - }, - ], - }); - const issues = validateScenarioGraph(graph); - expect(issues.some((i) => i.message.includes('not found'))).toBe(true); - }); - - it('warns about unreachable scenes', () => { - const graph = makeGraph({ - scenes: [ - { name: 'SCENE_1', description: '', durationMax: 300, actions: [], transitions: [] }, - { name: 'SCENE_2', description: '', durationMax: 300, actions: [], transitions: [] }, - ], - }); - const issues = validateScenarioGraph(graph); - expect(issues.some((i) => i.message.includes('unreachable'))).toBe(true); - }); - - it('warns about puzzles without hints', () => { - const graph = makeGraph({ - scenes: [ - { name: 'SCENE_1', description: '', durationMax: 300, actions: [], transitions: [] }, - ], - puzzles: [{ id: 'p1', name: 'Test', type: 'qr', hints: [] }], - }); - const issues = validateScenarioGraph(graph); - expect(issues.some((i) => i.message.includes('no hints'))).toBe(true); - }); - - it('detects duplicate puzzle IDs', () => { - const graph = makeGraph({ - scenes: [ - { name: 'SCENE_1', description: '', durationMax: 300, actions: [], transitions: [] }, - ], - puzzles: [ - { id: 'p1', name: 'A', type: 'qr', hints: [{ level: 1, text: 'h' }] }, - { id: 'p1', name: 'B', type: 'button', hints: [{ level: 1, text: 'h' }] }, - ], - }); - const issues = validateScenarioGraph(graph); - expect(issues.some((i) => i.message.includes('Duplicate puzzle ID'))).toBe(true); - }); - - it('returns no errors for valid scenario', () => { - const graph = makeGraph({ - scenes: [ - { - name: 'SCENE_INTRO', - description: 'Intro', - durationMax: 300, - actions: [], - transitions: [{ targetScene: 'SCENE_END', condition: '' }], - }, - { - name: 'SCENE_END', - description: 'End', - durationMax: 300, - actions: [], - transitions: [], - }, - ], - puzzles: [{ id: 'p1', name: 'Test', type: 'qr', hints: [{ level: 1, text: 'hint' }] }], - npcActions: [{ type: 'say', text: 'Hello' }], - }); - const issues = validateScenarioGraph(graph); - expect(issues.filter((i) => i.severity === 'error')).toHaveLength(0); - }); -}); - -describe('formatValidationSummary', () => { - it('shows Ready for valid graph', () => { - const graph = makeGraph({ - scenes: [ - { name: 'S', description: '', durationMax: 300, actions: [], transitions: [] }, - ], - }); - const summary = formatValidationSummary(graph, []); - expect(summary).toContain('Ready'); - expect(summary).toContain('1 scenes'); - }); - - it('shows error count when errors exist', () => { - const graph = makeGraph({ - scenes: [ - { name: 'S', description: '', durationMax: 300, actions: [], transitions: [] }, - ], - }); - const issues = [ - { severity: 'error' as const, message: 'bad' }, - { severity: 'warning' as const, message: 'meh' }, - ]; - const summary = formatValidationSummary(graph, issues); - expect(summary).toContain('1 error'); - expect(summary).toContain('1 warning'); - }); - - it('shows warning count when only warnings', () => { - const graph = makeGraph({ - scenes: [ - { name: 'S', description: '', durationMax: 300, actions: [], transitions: [] }, - ], - }); - const issues = [{ severity: 'warning' as const, message: 'meh' }]; - const summary = formatValidationSummary(graph, issues); - expect(summary).toContain('1 warning'); - expect(summary).not.toContain('error'); - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/scenario-editor/yaml-snapshots.test.ts b/frontend-scratch-v2/src/__tests__/scenario-editor/yaml-snapshots.test.ts deleted file mode 100644 index d7a58f7..0000000 --- a/frontend-scratch-v2/src/__tests__/scenario-editor/yaml-snapshots.test.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import * as Blockly from 'blockly'; -import { ensureScenarioBlocks } from '../../components/ScenarioEditor/blocks/scene'; -import { registerPuzzleBlocks } from '../../components/ScenarioEditor/blocks/puzzle'; -import { registerNpcBlocks } from '../../components/ScenarioEditor/blocks/npc'; -import { registerHardwareBlocks } from '../../components/ScenarioEditor/blocks/hardware'; -import { registerDeployBlocks } from '../../components/ScenarioEditor/blocks/deploy'; -import { - buildScenarioGraph, - scenarioGraphToFirmwareYaml, - scenarioGraphToDisplayYaml, -} from '../../components/ScenarioEditor/generators/yaml'; -import YAML from 'yaml'; - -beforeAll(() => { - ensureScenarioBlocks(); - registerPuzzleBlocks(); - registerNpcBlocks(); - registerHardwareBlocks(); - registerDeployBlocks(); -}); - -function createWorkspace(): Blockly.Workspace { - return new Blockly.Workspace(); -} - -// ─── Helper: connect block to a statement input ─── - -function connectStatement( - parent: Blockly.Block, - inputName: string, - child: Blockly.Block, -): void { - const input = parent.getInput(inputName); - if (input?.connection && child.previousConnection) { - input.connection.connect(child.previousConnection); - } -} - -function connectValue( - parent: Blockly.Block, - inputName: string, - child: Blockly.Block, -): void { - const input = parent.getInput(inputName); - if (input?.connection && child.outputConnection) { - input.connection.connect(child.outputConnection); - } -} - -function chainBlocks(first: Blockly.Block, second: Blockly.Block): void { - if (first.nextConnection && second.previousConnection) { - first.nextConnection.connect(second.previousConnection); - } -} - -// ─── Scenario 1: Simple linear ─── - -describe('YAML snapshot: simple linear scenario', () => { - it('generates display YAML with 2 scenes, 1 transition, 1 NPC say', () => { - const ws = createWorkspace(); - - // Scene 1: INTRO with transition to FINALE - const scene1 = ws.newBlock('scenario_scene'); - scene1.setFieldValue('SCENE_INTRO', 'NAME'); - scene1.setFieldValue('Welcome to the escape room', 'DESCRIPTION'); - scene1.setFieldValue('120', 'DURATION_MAX'); - - const trans = ws.newBlock('scenario_transition'); - trans.setFieldValue('SCENE_FINALE', 'TARGET_SCENE'); - connectStatement(scene1, 'TRANSITIONS', trans); - - // Scene 2: FINALE - const scene2 = ws.newBlock('scenario_scene'); - scene2.setFieldValue('SCENE_FINALE', 'NAME'); - scene2.setFieldValue('Congratulations!', 'DESCRIPTION'); - scene2.setFieldValue('60', 'DURATION_MAX'); - - // NPC say (standalone top-level block) - const npcSay = ws.newBlock('npc_say'); - npcSay.setFieldValue('Bienvenue dans le laboratoire!', 'TEXT'); - npcSay.setFieldValue('amused', 'MOOD'); - - const graph = buildScenarioGraph(ws); - const displayYaml = scenarioGraphToDisplayYaml(graph); - const parsed = YAML.parse(displayYaml); - - // Verify scenes - expect(parsed.scenes).toHaveLength(2); - expect(parsed.scenes[0].name).toBe('SCENE_INTRO'); - expect(parsed.scenes[0].duration_max_s).toBe(120); - expect(parsed.scenes[0].transitions).toHaveLength(1); - expect(parsed.scenes[0].transitions[0].target).toBe('SCENE_FINALE'); - expect(parsed.scenes[1].name).toBe('SCENE_FINALE'); - - // Verify NPC - expect(parsed.npc).toBeDefined(); - expect(parsed.npc).toHaveLength(1); - expect(parsed.npc[0].type).toBe('say'); - expect(parsed.npc[0].text).toBe('Bienvenue dans le laboratoire!'); - expect(parsed.npc[0].mood).toBe('amused'); - - // Verify YAML string contains key sections - expect(displayYaml).toContain('scenes:'); - expect(displayYaml).toContain('SCENE_INTRO'); - expect(displayYaml).toContain('SCENE_FINALE'); - expect(displayYaml).toContain('npc:'); - - ws.dispose(); - }); - - it('generates firmware YAML with correct steps structure', () => { - const ws = createWorkspace(); - - const scene1 = ws.newBlock('scenario_scene'); - scene1.setFieldValue('SCENE_INTRO', 'NAME'); - - const trans = ws.newBlock('scenario_transition'); - trans.setFieldValue('SCENE_FINALE', 'TARGET_SCENE'); - connectStatement(scene1, 'TRANSITIONS', trans); - - const scene2 = ws.newBlock('scenario_scene'); - scene2.setFieldValue('SCENE_FINALE', 'NAME'); - - const graph = buildScenarioGraph(ws); - const firmwareYaml = scenarioGraphToFirmwareYaml(graph); - const parsed = YAML.parse(firmwareYaml); - - expect(parsed.firmware).toBeDefined(); - expect(parsed.firmware.steps).toHaveLength(2); - expect(parsed.firmware.initial_step).toBe('SCENE_INTRO'); - expect(parsed.firmware.steps[0].step_id).toBe('SCENE_INTRO'); - expect(parsed.firmware.steps[0].transitions[0].target_step_id).toBe('SCENE_FINALE'); - expect(parsed.firmware.steps[1].step_id).toBe('SCENE_FINALE'); - - ws.dispose(); - }); -}); - -// ─── Scenario 2: Branching puzzle ─── - -describe('YAML snapshot: branching puzzle scenario', () => { - it('generates display YAML with 3 scenes, puzzle with QR, 2 transitions, hints', () => { - const ws = createWorkspace(); - - // Scene 1: LOBBY with two transitions (success → LAB, timeout → GAMEOVER) - const scene1 = ws.newBlock('scenario_scene'); - scene1.setFieldValue('SCENE_LOBBY', 'NAME'); - scene1.setFieldValue('The lobby area', 'DESCRIPTION'); - scene1.setFieldValue('300', 'DURATION_MAX'); - - const transSuccess = ws.newBlock('scenario_transition'); - transSuccess.setFieldValue('SCENE_LAB', 'TARGET_SCENE'); - - const transTimeout = ws.newBlock('scenario_transition'); - transTimeout.setFieldValue('SCENE_GAMEOVER', 'TARGET_SCENE'); - - connectStatement(scene1, 'TRANSITIONS', transSuccess); - chainBlocks(transSuccess, transTimeout); - - // Scene 2: LAB - const scene2 = ws.newBlock('scenario_scene'); - scene2.setFieldValue('SCENE_LAB', 'NAME'); - scene2.setFieldValue('The secret laboratory', 'DESCRIPTION'); - scene2.setFieldValue('600', 'DURATION_MAX'); - - // Scene 3: GAMEOVER - const scene3 = ws.newBlock('scenario_scene'); - scene3.setFieldValue('SCENE_GAMEOVER', 'NAME'); - scene3.setFieldValue('Time is up!', 'DESCRIPTION'); - scene3.setFieldValue('30', 'DURATION_MAX'); - - // Puzzle with QR validation and hints - const puzzle = ws.newBlock('puzzle_definition'); - puzzle.setFieldValue('PUZZLE_ENTRANCE', 'NAME'); - puzzle.setFieldValue('qr', 'PUZZLE_TYPE'); - - const qrValidation = ws.newBlock('puzzle_validation_qr'); - qrValidation.setFieldValue('ZACUS_DOOR_KEY', 'EXPECTED'); - connectValue(puzzle, 'SOLUTION', qrValidation); - - // Hints inside the puzzle - const hint1 = ws.newBlock('npc_hint'); - hint1.setFieldValue('1', 'LEVEL'); - hint1.setFieldValue('PUZZLE_ENTRANCE', 'PUZZLE_ID'); - hint1.setFieldValue('Look at the painting on the wall', 'TEXT'); - - const hint2 = ws.newBlock('npc_hint'); - hint2.setFieldValue('2', 'LEVEL'); - hint2.setFieldValue('PUZZLE_ENTRANCE', 'PUZZLE_ID'); - hint2.setFieldValue('The QR code is behind the frame', 'TEXT'); - - connectStatement(puzzle, 'HINTS', hint1); - chainBlocks(hint1, hint2); - - const graph = buildScenarioGraph(ws); - const displayYaml = scenarioGraphToDisplayYaml(graph); - const parsed = YAML.parse(displayYaml); - - // Scenes - expect(parsed.scenes).toHaveLength(3); - expect(parsed.scenes[0].name).toBe('SCENE_LOBBY'); - expect(parsed.scenes[0].transitions).toHaveLength(2); - expect(parsed.scenes[0].transitions[0].target).toBe('SCENE_LAB'); - expect(parsed.scenes[0].transitions[1].target).toBe('SCENE_GAMEOVER'); - - // Puzzle - expect(parsed.puzzles).toBeDefined(); - expect(parsed.puzzles).toHaveLength(1); - expect(parsed.puzzles[0].name).toBe('PUZZLE_ENTRANCE'); - expect(parsed.puzzles[0].type).toBe('qr'); - expect(parsed.puzzles[0].solution).toBe('qr:ZACUS_DOOR_KEY'); - expect(parsed.puzzles[0].hints).toHaveLength(2); - expect(parsed.puzzles[0].hints[0].level).toBe(1); - expect(parsed.puzzles[0].hints[0].text).toBe('Look at the painting on the wall'); - expect(parsed.puzzles[0].hints[1].level).toBe(2); - - // YAML string checks - expect(displayYaml).toContain('puzzles:'); - expect(displayYaml).toContain('PUZZLE_ENTRANCE'); - expect(displayYaml).toContain('qr:ZACUS_DOOR_KEY'); - - ws.dispose(); - }); - - it('generates firmware YAML with puzzles section', () => { - const ws = createWorkspace(); - - const scene1 = ws.newBlock('scenario_scene'); - scene1.setFieldValue('SCENE_LOBBY', 'NAME'); - - const puzzle = ws.newBlock('puzzle_definition'); - puzzle.setFieldValue('PUZZLE_QR', 'NAME'); - puzzle.setFieldValue('qr', 'PUZZLE_TYPE'); - - const qr = ws.newBlock('puzzle_validation_qr'); - qr.setFieldValue('KEY_42', 'EXPECTED'); - connectValue(puzzle, 'SOLUTION', qr); - - const hint = ws.newBlock('npc_hint'); - hint.setFieldValue('1', 'LEVEL'); - hint.setFieldValue('PUZZLE_QR', 'PUZZLE_ID'); - hint.setFieldValue('Check under the desk', 'TEXT'); - connectStatement(puzzle, 'HINTS', hint); - - const graph = buildScenarioGraph(ws); - const firmwareYaml = scenarioGraphToFirmwareYaml(graph); - const parsed = YAML.parse(firmwareYaml); - - expect(parsed.firmware.steps).toHaveLength(1); - expect(parsed.firmware.puzzles).toBeDefined(); - expect(parsed.firmware.puzzles).toHaveLength(1); - expect(parsed.firmware.puzzles[0].puzzle_id).toBe('PUZZLE_QR'); - expect(parsed.firmware.puzzles[0].type).toBe('qr'); - expect(parsed.firmware.puzzles[0].solution).toBe('qr:KEY_42'); - expect(parsed.firmware.puzzles[0].hints_count).toBe(1); - - ws.dispose(); - }); -}); - -// ─── Scenario 3: Full escape room ─── - -describe('YAML snapshot: full escape room scenario', () => { - it('generates display YAML with all sections', () => { - const ws = createWorkspace(); - - // Scene 1: INTRO with timer action - const scene1 = ws.newBlock('scenario_scene'); - scene1.setFieldValue('SCENE_INTRO', 'NAME'); - scene1.setFieldValue('The adventure begins', 'DESCRIPTION'); - scene1.setFieldValue('180', 'DURATION_MAX'); - - const timer = ws.newBlock('scenario_timer'); - timer.setFieldValue('60', 'SECONDS'); - const varSet = ws.newBlock('scenario_variable_set'); - varSet.setFieldValue('intro_timeout', 'NAME'); - connectStatement(timer, 'ON_EXPIRE', varSet); - connectStatement(scene1, 'ACTIONS', timer); - - const trans1 = ws.newBlock('scenario_transition'); - trans1.setFieldValue('SCENE_LAB', 'TARGET_SCENE'); - connectStatement(scene1, 'TRANSITIONS', trans1); - - // Scene 2: LAB - const scene2 = ws.newBlock('scenario_scene'); - scene2.setFieldValue('SCENE_LAB', 'NAME'); - scene2.setFieldValue('Professor Zacus laboratory', 'DESCRIPTION'); - scene2.setFieldValue('600', 'DURATION_MAX'); - - const trans2 = ws.newBlock('scenario_transition'); - trans2.setFieldValue('SCENE_ESCAPE', 'TARGET_SCENE'); - connectStatement(scene2, 'TRANSITIONS', trans2); - - // Scene 3: ESCAPE - const scene3 = ws.newBlock('scenario_scene'); - scene3.setFieldValue('SCENE_ESCAPE', 'NAME'); - scene3.setFieldValue('Find the exit!', 'DESCRIPTION'); - scene3.setFieldValue('300', 'DURATION_MAX'); - - // Puzzle 1: QR - const puzzle1 = ws.newBlock('puzzle_definition'); - puzzle1.setFieldValue('PUZZLE_DOOR', 'NAME'); - puzzle1.setFieldValue('qr', 'PUZZLE_TYPE'); - - const qr = ws.newBlock('puzzle_validation_qr'); - qr.setFieldValue('ZACUS_EXIT', 'EXPECTED'); - connectValue(puzzle1, 'SOLUTION', qr); - - const hint = ws.newBlock('npc_hint'); - hint.setFieldValue('1', 'LEVEL'); - hint.setFieldValue('PUZZLE_DOOR', 'PUZZLE_ID'); - hint.setFieldValue('Search the bookshelf', 'TEXT'); - connectStatement(puzzle1, 'HINTS', hint); - - // Puzzle 2: Button - const puzzle2 = ws.newBlock('puzzle_definition'); - puzzle2.setFieldValue('PUZZLE_SAFE', 'NAME'); - puzzle2.setFieldValue('button', 'PUZZLE_TYPE'); - - const btnVal = ws.newBlock('puzzle_validation_button'); - btnVal.setFieldValue('12', 'PIN'); - connectValue(puzzle2, 'SOLUTION', btnVal); - - // NPC dialogue - const npcSay = ws.newBlock('npc_say'); - npcSay.setFieldValue('Bienvenue dans mon laboratoire!', 'TEXT'); - npcSay.setFieldValue('impressed', 'MOOD'); - - // Hardware: GPIO write - const gpioWrite = ws.newBlock('hw_gpio_write'); - gpioWrite.setFieldValue('15', 'PIN'); - gpioWrite.setFieldValue('HIGH', 'STATE'); - - // Deploy config: WiFi + TTS + LLM - const wifi = ws.newBlock('deploy_config_wifi'); - wifi.setFieldValue('ZacusNet', 'SSID'); - wifi.setFieldValue('secret123', 'PASSWORD'); - - const tts = ws.newBlock('deploy_config_tts'); - tts.setFieldValue('http://192.168.0.120:8001', 'URL'); - tts.setFieldValue('tom-medium', 'VOICE'); - - const llm = ws.newBlock('deploy_config_llm'); - llm.setFieldValue('http://kxkm-ai:11434', 'URL'); - llm.setFieldValue('devstral', 'MODEL'); - - const graph = buildScenarioGraph(ws); - const displayYaml = scenarioGraphToDisplayYaml(graph); - const parsed = YAML.parse(displayYaml); - - // Scenes - expect(parsed.scenes).toHaveLength(3); - expect(parsed.scenes[0].name).toBe('SCENE_INTRO'); - expect(parsed.scenes[0].actions).toHaveLength(1); - expect(parsed.scenes[0].actions[0].type).toBe('timer'); - expect(parsed.scenes[0].actions[0].seconds).toBe(60); - - // Puzzles - expect(parsed.puzzles).toHaveLength(2); - expect(parsed.puzzles[0].name).toBe('PUZZLE_DOOR'); - expect(parsed.puzzles[0].solution).toBe('qr:ZACUS_EXIT'); - expect(parsed.puzzles[1].name).toBe('PUZZLE_SAFE'); - expect(parsed.puzzles[1].type).toBe('button'); - expect(parsed.puzzles[1].solution).toBe('button:12'); - - // NPC - expect(parsed.npc).toBeDefined(); - expect(parsed.npc.length).toBeGreaterThanOrEqual(1); - const sayAction = parsed.npc.find( - (a: { type: string }) => a.type === 'say', - ); - expect(sayAction).toBeDefined(); - expect(sayAction.text).toBe('Bienvenue dans mon laboratoire!'); - - // Hardware - expect(parsed.hardware).toBeDefined(); - expect(parsed.hardware.length).toBeGreaterThanOrEqual(1); - const gpio = parsed.hardware.find( - (a: { type: string }) => a.type === 'gpio_write', - ); - expect(gpio).toBeDefined(); - expect(gpio.pin).toBe(15); - expect(gpio.state).toBe('HIGH'); - - // Deploy - expect(parsed.deploy).toBeDefined(); - expect(parsed.deploy.wifi).toEqual({ ssid: 'ZacusNet', password: 'secret123' }); - expect(parsed.deploy.tts).toEqual({ - url: 'http://192.168.0.120:8001', - voice: 'tom-medium', - }); - expect(parsed.deploy.llm).toEqual({ - url: 'http://kxkm-ai:11434', - model: 'devstral', - }); - - // Verify all display YAML sections exist - expect(displayYaml).toContain('scenes:'); - expect(displayYaml).toContain('puzzles:'); - expect(displayYaml).toContain('npc:'); - expect(displayYaml).toContain('hardware:'); - expect(displayYaml).toContain('deploy:'); - - ws.dispose(); - }); - - it('generates firmware YAML with hardware actions attached to first step', () => { - const ws = createWorkspace(); - - const scene1 = ws.newBlock('scenario_scene'); - scene1.setFieldValue('SCENE_BOOT', 'NAME'); - - const scene2 = ws.newBlock('scenario_scene'); - scene2.setFieldValue('SCENE_PLAY', 'NAME'); - - // Hardware - const gpio = ws.newBlock('hw_gpio_write'); - gpio.setFieldValue('15', 'PIN'); - gpio.setFieldValue('HIGH', 'STATE'); - - const led = ws.newBlock('hw_led_set'); - led.setFieldValue('#FF0000', 'COLOR'); - led.setFieldValue('blink', 'ANIMATION'); - - // Deploy - const wifi = ws.newBlock('deploy_config_wifi'); - wifi.setFieldValue('EscapeNet', 'SSID'); - wifi.setFieldValue('pass', 'PASSWORD'); - - const graph = buildScenarioGraph(ws); - const firmwareYaml = scenarioGraphToFirmwareYaml(graph); - const parsed = YAML.parse(firmwareYaml); - - // firmware.steps[] structure - expect(parsed.firmware).toBeDefined(); - expect(parsed.firmware.steps).toHaveLength(2); - expect(parsed.firmware.initial_step).toBe('SCENE_BOOT'); - - // Hardware actions on first step - expect(parsed.firmware.steps[0].actions).toHaveLength(2); - expect(parsed.firmware.steps[0].actions[0].type).toBe('gpio_write'); - expect(parsed.firmware.steps[0].actions[0].pin).toBe(15); - expect(parsed.firmware.steps[0].actions[1].type).toBe('led_set'); - expect(parsed.firmware.steps[0].actions[1].color).toBe('#FF0000'); - - // Deploy config in firmware - expect(parsed.firmware.deploy).toBeDefined(); - expect(parsed.firmware.deploy.wifi.ssid).toBe('EscapeNet'); - - ws.dispose(); - }); -}); diff --git a/frontend-scratch-v2/src/__tests__/scenario.test.ts b/frontend-scratch-v2/src/__tests__/scenario.test.ts deleted file mode 100644 index b770728..0000000 --- a/frontend-scratch-v2/src/__tests__/scenario.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - buildScenarioFromBlocks, - validateScenarioDocument, - normalizeId, -} from '../lib/scenario'; -import type { ScenarioStep, ScenarioDocument } from '../types'; - -// ─── Helpers ─── - -function makeValidSteps(overrides?: Partial[]): ScenarioStep[] { - const defaults: ScenarioStep[] = [ - { - stepId: 'STEP_A', - sceneId: 'SCENE_A', - transitions: [ - { - eventType: 'button', - eventName: 'BTN_NEXT', - targetStepId: 'STEP_B', - priority: 0, - afterMs: 0, - }, - ], - }, - { - stepId: 'STEP_B', - sceneId: 'SCENE_B', - transitions: [], - }, - ]; - if (!overrides) return defaults; - return overrides.map((o, i) => ({ ...defaults[i % defaults.length], ...o })); -} - -function makeValidDocument( - steps?: ScenarioStep[], - scenarioId = 'TEST_SCENARIO', -): ScenarioDocument { - return buildScenarioFromBlocks(scenarioId, steps ?? makeValidSteps()); -} - -// ─── Tests ─── - -describe('scenario validation', () => { - it('valid scenario passes validation', () => { - const doc = makeValidDocument(); - const result = validateScenarioDocument(doc); - expect(result).toEqual({ ok: true }); - }); - - it('missing required field (empty id) fails', () => { - const doc = makeValidDocument(makeValidSteps(), ''); - // buildScenarioFromBlocks normalizes empty to fallback, so force it - doc.id = ''; - const result = validateScenarioDocument(doc); - expect(result.ok).toBe(false); - }); - - it('missing title fails', () => { - const doc = makeValidDocument(); - doc.title = ''; - const result = validateScenarioDocument(doc); - expect(result.ok).toBe(false); - }); - - it('duplicate step IDs detected', () => { - const doc = makeValidDocument(); - // Manually inject duplicate step IDs into firmware - if (doc.firmware?.steps) { - doc.firmware.steps.push({ ...doc.firmware.steps[0] }); - doc.firmware.steps_reference_order = doc.firmware.steps.map( - (s) => s.step_id, - ); - } - doc.steps_narrative.push({ ...doc.steps_narrative[0] }); - const result = validateScenarioDocument(doc); - expect(result.ok).toBe(false); - expect('error' in result && result.error).toContain('duplicate'); - }); - - it('transition target pointing to existing step passes', () => { - const steps: ScenarioStep[] = [ - { - stepId: 'STEP_1', - sceneId: 'SCENE_1', - transitions: [ - { - eventType: 'serial', - eventName: 'BTN_GO', - targetStepId: 'STEP_2', - priority: 0, - afterMs: 0, - }, - ], - }, - { stepId: 'STEP_2', sceneId: 'SCENE_2', transitions: [] }, - ]; - const doc = makeValidDocument(steps); - expect(validateScenarioDocument(doc)).toEqual({ ok: true }); - }); - - it('transition target pointing to non-existing step fails', () => { - const steps: ScenarioStep[] = [ - { - stepId: 'STEP_1', - sceneId: 'SCENE_1', - transitions: [ - { - eventType: 'serial', - eventName: 'BTN_GO', - targetStepId: 'STEP_GHOST', - priority: 0, - afterMs: 0, - }, - ], - }, - ]; - const doc = makeValidDocument(steps); - const result = validateScenarioDocument(doc); - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toContain('STEP_GHOST'); - } - }); - - it('players.min > players.max fails', () => { - const doc = makeValidDocument(); - doc.players = { min: 20, max: 5 }; - const result = validateScenarioDocument(doc); - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toContain('players'); - } - }); - - it('empty steps array produces fallback STEP_BOOT', () => { - const doc = makeValidDocument([]); - expect(doc.firmware?.steps).toHaveLength(1); - expect(doc.firmware?.steps?.[0].step_id).toBe('STEP_BOOT'); - expect(validateScenarioDocument(doc)).toEqual({ ok: true }); - }); -}); - -describe('normalizeId', () => { - it('empty string returns fallback', () => { - expect(normalizeId('', 'FALLBACK')).toBe('FALLBACK'); - }); - - it('whitespace-only returns fallback', () => { - expect(normalizeId(' ', 'FB')).toBe('FB'); - }); - - it('special characters are replaced with underscores', () => { - expect(normalizeId('hello-world!@#', 'X')).toBe('HELLO_WORLD'); - }); - - it('unicode characters are stripped', () => { - const result = normalizeId('etape_cafe', 'X'); - expect(result).toBe('ETAPE_CAFE'); - }); - - it('leading/trailing underscores are removed', () => { - expect(normalizeId('__test__', 'X')).toBe('TEST'); - }); - - it('already valid ID is uppercased', () => { - expect(normalizeId('step_one', 'X')).toBe('STEP_ONE'); - }); -}); diff --git a/frontend-scratch-v2/src/assets/react.svg b/frontend-scratch-v2/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend-scratch-v2/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend-scratch-v2/src/components/BlocklyDesigner.tsx b/frontend-scratch-v2/src/components/BlocklyDesigner.tsx deleted file mode 100644 index 12abe85..0000000 --- a/frontend-scratch-v2/src/components/BlocklyDesigner.tsx +++ /dev/null @@ -1,389 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import * as Blockly from 'blockly'; -import 'blockly/blocks'; -import { - buildScenarioFromBlocks, - scenarioToYaml, - validateScenarioDocument, - parseYamlToSteps, -} from '../lib/scenario'; -import { - compileScenarioDocumentToRuntime3, - runtime3ToJson, - validateRuntime3Document, -} from '../lib/runtime3'; -import type { ScenarioStep } from '../types'; - -const TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = { - kind: 'categoryToolbox', - contents: [ - { - kind: 'category', - name: 'Steps', - colour: '#3b82f6', - contents: [{ kind: 'block', type: 'zacus_step' }], - }, - { - kind: 'category', - name: 'Transitions', - colour: '#22c55e', - contents: [{ kind: 'block', type: 'zacus_transition' }], - }, - ], -}; - -let blocksRegistered = false; - -function ensureCustomBlocks(): void { - if (blocksRegistered) return; - - Blockly.defineBlocksWithJsonArray([ - { - type: 'zacus_step', - message0: 'Step %1 Scene %2 Audio %3', - args0: [ - { type: 'field_input', name: 'STEP_ID', text: 'STEP_NEW' }, - { type: 'field_input', name: 'SCENE_ID', text: 'SCENE_NEW' }, - { type: 'field_input', name: 'AUDIO_PACK', text: '' }, - ], - message1: '%1', - args1: [{ type: 'input_statement', name: 'TRANSITIONS' }], - previousStatement: null, - nextStatement: null, - colour: 210, - tooltip: 'A scenario step with its screen scene and audio pack', - }, - { - type: 'zacus_transition', - message0: 'on %1 : %2 → %3 after %4 ms prio %5', - args0: [ - { - type: 'field_dropdown', - name: 'EVENT_TYPE', - options: [ - ['button', 'button'], - ['serial', 'serial'], - ['timer', 'timer'], - ['audio_done', 'audio_done'], - ['unlock', 'unlock'], - ['espnow', 'espnow'], - ['action', 'action'], - ], - }, - { type: 'field_input', name: 'EVENT_NAME', text: 'BTN_NEXT' }, - { type: 'field_input', name: 'TARGET', text: 'STEP_NEXT' }, - { type: 'field_number', name: 'AFTER_MS', value: 0, min: 0, precision: 1 }, - { type: 'field_number', name: 'PRIORITY', value: 0, min: 0, precision: 1 }, - ], - previousStatement: null, - nextStatement: null, - colour: 140, - tooltip: 'A transition triggered by an event', - }, - ]); - - blocksRegistered = true; -} - -function readScenarioSteps(workspace: Blockly.WorkspaceSvg): ScenarioStep[] { - const steps: ScenarioStep[] = []; - const seenBlocks = new Set(); - - const readTransitions = (block: Blockly.Block): ScenarioStep['transitions'] => { - const transitions: NonNullable = []; - let transitionBlock = block.getInputTargetBlock('TRANSITIONS'); - while (transitionBlock) { - if (transitionBlock.type === 'zacus_transition') { - transitions.push({ - eventType: transitionBlock.getFieldValue('EVENT_TYPE') ?? 'serial', - eventName: transitionBlock.getFieldValue('EVENT_NAME') ?? 'BTN_NEXT', - targetStepId: transitionBlock.getFieldValue('TARGET') ?? 'STEP_NEXT', - afterMs: Number(transitionBlock.getFieldValue('AFTER_MS') ?? 0), - priority: Number(transitionBlock.getFieldValue('PRIORITY') ?? 0), - }); - } - transitionBlock = transitionBlock.getNextBlock(); - } - return transitions; - }; - - const collectChain = (startBlock: Blockly.Block): void => { - let cursor: Blockly.Block | null = startBlock; - while (cursor) { - if (seenBlocks.has(cursor.id)) break; - seenBlocks.add(cursor.id); - - if (cursor.type === 'zacus_step') { - steps.push({ - stepId: cursor.getFieldValue('STEP_ID') ?? '', - sceneId: cursor.getFieldValue('SCENE_ID') ?? '', - audioPack: cursor.getFieldValue('AUDIO_PACK') ?? '', - transitions: readTransitions(cursor), - }); - } - cursor = cursor.getNextBlock(); - } - }; - - for (const block of workspace.getTopBlocks(true)) { - if (block.type === 'zacus_step') collectChain(block); - } - - if (steps.length === 0) { - for (const block of workspace.getAllBlocks(false)) { - if (block.type === 'zacus_step') collectChain(block); - } - } - - // Deduplicate step IDs — keep first occurrence, warn on duplicates - const seenStepIds = new Set(); - const deduped: ScenarioStep[] = []; - for (const step of steps) { - if (seenStepIds.has(step.stepId)) { - console.warn( - `[BlocklyDesigner] Duplicate step ID "${step.stepId}" detected — skipping duplicate.`, - ); - continue; - } - seenStepIds.add(step.stepId); - deduped.push(step); - } - - return deduped; -} - -function loadStepsIntoWorkspace( - workspace: Blockly.WorkspaceSvg, - steps: ScenarioStep[], -): void { - workspace.clear(); - let prevBlock: Blockly.Block | null = null; - const Y_START = 40; - const X_START = 40; - - for (const step of steps) { - const block = workspace.newBlock('zacus_step'); - block.setFieldValue(step.stepId || 'STEP_NEW', 'STEP_ID'); - block.setFieldValue(step.sceneId || 'SCENE_NEW', 'SCENE_ID'); - block.setFieldValue(step.audioPack || '', 'AUDIO_PACK'); - block.initSvg(); - block.render(); - - const transitions = step.transitions ?? []; - let previousTransitionBlock: Blockly.Block | null = null; - for (const transition of transitions) { - const transitionBlock = workspace.newBlock('zacus_transition'); - transitionBlock.setFieldValue(transition.eventType, 'EVENT_TYPE'); - transitionBlock.setFieldValue(transition.eventName, 'EVENT_NAME'); - transitionBlock.setFieldValue(transition.targetStepId, 'TARGET'); - transitionBlock.setFieldValue(String(transition.afterMs), 'AFTER_MS'); - transitionBlock.setFieldValue(String(transition.priority), 'PRIORITY'); - transitionBlock.initSvg(); - transitionBlock.render(); - - if (!previousTransitionBlock) { - const inputConnection = block.getInput('TRANSITIONS')?.connection; - if (inputConnection && transitionBlock.previousConnection) { - inputConnection.connect(transitionBlock.previousConnection); - } - } else if ( - previousTransitionBlock.nextConnection && - transitionBlock.previousConnection - ) { - previousTransitionBlock.nextConnection.connect( - transitionBlock.previousConnection, - ); - } - - previousTransitionBlock = transitionBlock; - } - - if (prevBlock) { - if (prevBlock.nextConnection && block.previousConnection) { - prevBlock.nextConnection.connect(block.previousConnection); - } - } else { - block.moveBy(X_START, Y_START); - } - prevBlock = block; - } -} - -function addStarterBlocks(workspace: Blockly.WorkspaceSvg): void { - loadStepsIntoWorkspace(workspace, [ - { - stepId: 'STEP_U_SON_PROTO', - sceneId: 'SCENE_U_SON_PROTO', - transitions: [ - { - eventType: 'button', - eventName: 'ANY', - targetStepId: 'STEP_LA_DETECTOR', - priority: 0, - afterMs: 0, - }, - ], - }, - { stepId: 'STEP_LA_DETECTOR', sceneId: 'SCENE_LA_DETECTOR', transitions: [] }, - ]); -} - -type BlocklyDesignerProps = { - onDraftChange: (draft: { yaml: string; runtime3Json: string }) => void; -}; - -export function BlocklyDesigner({ onDraftChange }: BlocklyDesignerProps) { - const hostRef = useRef(null); - const workspaceRef = useRef(null); - const [scenarioId, setScenarioId] = useState('zacus_v2_new'); - const [steps, setSteps] = useState([]); - const [copyInfo, setCopyInfo] = useState(''); - const fileInputRef = useRef(null); - - useEffect(() => { - if (!hostRef.current) return; - - ensureCustomBlocks(); - const workspace = Blockly.inject(hostRef.current, { - toolbox: TOOLBOX, - trashcan: true, - grid: { spacing: 20, length: 3, colour: '#3a3f52', snap: true }, - zoom: { controls: true, wheel: true, startScale: 0.95 }, - }); - workspaceRef.current = workspace; - addStarterBlocks(workspace); - - const onChange = () => { - setSteps(readScenarioSteps(workspace)); - setCopyInfo(''); - }; - - workspace.addChangeListener(onChange); - onChange(); - - return () => { - workspace.removeChangeListener(onChange); - workspace.dispose(); - workspaceRef.current = null; - }; - }, []); - - const generated = useMemo(() => { - const scenarioDocument = buildScenarioFromBlocks(scenarioId, steps); - const runtime3Document = compileScenarioDocumentToRuntime3(scenarioDocument); - return { - scenarioDocument, - yaml: scenarioToYaml(scenarioDocument), - validation: validateScenarioDocument(scenarioDocument), - runtime3Json: runtime3ToJson(runtime3Document), - runtime3Validation: validateRuntime3Document(runtime3Document), - }; - }, [scenarioId, steps]); - - useEffect(() => { - onDraftChange({ - yaml: generated.yaml, - runtime3Json: generated.runtime3Json, - }); - }, [generated.runtime3Json, generated.yaml, onDraftChange]); - - const handleReset = () => { - if (!workspaceRef.current) return; - addStarterBlocks(workspaceRef.current); - setSteps(readScenarioSteps(workspaceRef.current)); - }; - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(generated.yaml); - setCopyInfo('YAML copied.'); - } catch { - setCopyInfo('Copy failed.'); - } - }; - - const handleImportYaml = (yamlStr: string) => { - const result = parseYamlToSteps(yamlStr); - if ('error' in result) { - setCopyInfo(`Import error: ${result.error}`); - return; - } - setScenarioId(result.id); - if (workspaceRef.current) { - loadStepsIntoWorkspace(workspaceRef.current, result.steps); - setSteps(readScenarioSteps(workspaceRef.current)); - } - setCopyInfo(`Imported ${result.steps.length} steps from "${result.id}".`); - }; - - const handleFileImport = () => { - fileInputRef.current?.click(); - }; - - const handleFileChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = () => { - if (typeof reader.result === 'string') { - handleImportYaml(reader.result); - } - }; - reader.readAsText(file); - e.target.value = ''; - }; - - return ( - <> -
- setScenarioId(event.target.value)} - aria-label="scenario id" - placeholder="scenario id" - /> - - - - -
- -
- -
- {generated.validation.ok && generated.runtime3Validation.ok - ? `Ready — ${steps.length} step(s), ${ - steps.reduce( - (total, step) => total + (step.transitions?.length ?? 0), - 0, - ) - } transition(s). ${copyInfo}` - : `Invalid: ${ - !generated.validation.ok - ? generated.validation.error - : generated.runtime3Validation.ok - ? 'runtime3 validation error' - : generated.runtime3Validation.error - }`} -
- - ); -} diff --git a/frontend-scratch-v2/src/components/Dashboard.tsx b/frontend-scratch-v2/src/components/Dashboard.tsx deleted file mode 100644 index a58c53e..0000000 --- a/frontend-scratch-v2/src/components/Dashboard.tsx +++ /dev/null @@ -1,534 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { - type ScenarioListItem, - type VoiceStatus, - type GameAnalytics, - storyList, - storySelect, - storyStart, - storyPause, - storyResume, - storySkip, - askHint, - voiceStatus, - gameAnalytics, -} from '../lib/api'; -import type { RuntimeState } from '../lib/useRuntimeStore'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -interface ChatMessage { - role: 'user' | 'zacus'; - text: string; - timestamp: number; -} - -type Props = { runtime: RuntimeState; refresh: () => void }; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function Dashboard({ runtime, refresh }: Props) { - const [scenarios, setScenarios] = useState([]); - const [busy, setBusy] = useState(false); - const [feedback, setFeedback] = useState(''); - const [voiceInput, setVoiceInput] = useState(''); - const [voice, setVoice] = useState(null); - - // Chat history - const [chatHistory, setChatHistory] = useState([]); - const [chatLoading, setChatLoading] = useState(false); - const chatEndRef = useRef(null); - - // Hint level - const [hintLevel, setHintLevel] = useState(1); - - // Analytics - const [analytics, setAnalytics] = useState(null); - const [analyticsError, setAnalyticsError] = useState(''); - - // ─── Initial load ─── - useEffect(() => { - storyList() - .then(setScenarios) - .catch(() => setScenarios([])); - voiceStatus() - .then(setVoice) - .catch(() => setVoice(null)); - }, [runtime.connected]); - - // ─── Auto-poll voice status every 5s when connected ─── - useEffect(() => { - if (!runtime.connected) return; - const id = setInterval(() => { - voiceStatus().then(setVoice).catch(() => {}); - }, 5000); - return () => clearInterval(id); - }, [runtime.connected]); - - // ─── Scroll chat to bottom ─── - useEffect(() => { - chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [chatHistory, chatLoading]); - - // ─── Helpers ─── - const run = async (label: string, fn: () => Promise) => { - setBusy(true); - setFeedback(''); - try { - await fn(); - setFeedback(`${label} OK`); - refresh(); - } catch (err) { - setFeedback( - `${label} failed: ${err instanceof Error ? err.message : err}`, - ); - } finally { - setBusy(false); - } - }; - - const handleAskZacus = async () => { - const text = voiceInput.trim(); - if (!text) return; - - const userMsg: ChatMessage = { role: 'user', text, timestamp: Date.now() }; - setChatHistory((prev) => [...prev, userMsg]); - setVoiceInput(''); - setChatLoading(true); - - try { - const puzzleId = runtime.story?.current_step || 'general'; - const res = await askHint(puzzleId, text, hintLevel); - const zacusMsg: ChatMessage = { - role: 'zacus', - text: res.hint, - timestamp: Date.now(), - }; - setChatHistory((prev) => [...prev, zacusMsg]); - } catch (err) { - const errorMsg: ChatMessage = { - role: 'zacus', - text: `[Error] ${err instanceof Error ? err.message : String(err)}`, - timestamp: Date.now(), - }; - setChatHistory((prev) => [...prev, errorMsg]); - } finally { - setChatLoading(false); - } - }; - - const refreshAnalytics = () => { - setAnalyticsError(''); - gameAnalytics() - .then(setAnalytics) - .catch((err) => - setAnalyticsError(err instanceof Error ? err.message : String(err)), - ); - }; - - const formatDuration = (ms: number) => { - const s = Math.floor(ms / 1000); - const m = Math.floor(s / 60); - const sec = s % 60; - return `${m}m ${sec}s`; - }; - - const story = runtime.story; - const legacy = runtime.legacy; - const net = legacy?.network; - const runtime3 = legacy?.runtime3; - - return ( -
-

Dashboard

- - {/* Connection */} -
-

Connection

-
- Status - - {runtime.connected ? 'Connected' : 'Disconnected'} - -
- {net && ( - <> -
- Network - {net.state ?? '?'} -
-
- IP - {net.ip ?? '?'} -
- - )} -
- - {/* Story status */} -
-

Story runtime

- {story ? ( - <> -
- Status - {story.status} -
-
- Scenario - {story.scenario_id || '\u2014'} -
-
- Step - {story.current_step || '\u2014'} -
-
- Progress -
-
- {story.progress_pct}% -
-
-
- Queue - {story.queue_depth} -
- - ) : ( -

No Story V2 status available.

- )} -
- -
-

Runtime 3 adapter

- {runtime3 ? ( -
-
- Contract - - {legacy?.story.runtime_contract ?? 'story_v2'} - -
-
- Status - - {runtime3.loaded - ? 'Loaded' - : runtime3.discovered - ? 'Discovered' - : 'Missing'} - -
-
- Scenario - {runtime3.scenario_id || '\u2014'} -
-
- Version - {runtime3.scenario_version || 0} -
-
- Entry step - {runtime3.entry_step_id || '\u2014'} -
-
- Migration - {runtime3.migration_mode || '\u2014'} -
-
- Steps - {runtime3.step_count} -
-
- Transitions - {runtime3.transition_count} -
-
- Source - {runtime3.source_kind || '\u2014'} -
-
- Schema - {runtime3.schema_version || '\u2014'} -
-
- Bundle - {runtime3.path || '\u2014'} -
- {runtime3.error && ( -
- Runtime 3 error: {runtime3.error} -
- )} -
- ) : ( -

No Runtime 3 adapter metadata available.

- )} -
- - {/* Scenarios */} -
-

Scenarios

- {scenarios.length === 0 ? ( -

No scenarios found.

- ) : ( -
    - {scenarios.map((s) => ( -
  • - {s.id} - v{s.version} - -
  • - ))} -
- )} -
- - {/* Controls */} -
-

Controls

-
- - - - -
- {feedback &&

{feedback}

} -
- - {/* Voice — Ask Professor Zacus (enhanced chat) */} -
-

Ask Professor Zacus

-
- Voice Bridge - - {voice?.connected ? 'Connected' : 'Disconnected'} - -
- - {/* Hint level selector */} -
- Hint level: - {[1, 2, 3].map((lvl) => ( - - ))} -
- - {/* Chat history */} -
- {chatHistory.length === 0 && !chatLoading && ( -

- Ask Professor Zacus a question about the puzzle... -

- )} - {chatHistory.map((msg, i) => ( -
-
- {msg.role === 'zacus' && ( - - Prof. Zacus - - )} - {msg.text} -
- - {new Date(msg.timestamp).toLocaleTimeString()} - -
- ))} - {chatLoading && ( -
- - Professor Zacus is thinking... -
- )} -
-
- - {/* Hint count */} - {analytics && ( -
- Hints used this session: {analytics.total_hints} -
- )} - - {/* Input */} -
- setVoiceInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && voiceInput.trim() && !chatLoading) { - handleAskZacus(); - } - }} - disabled={chatLoading} - style={{ flex: 1 }} - /> - -
-
- - {/* Analytics */} -
-

- Analytics{' '} - -

- {analyticsError &&

{analyticsError}

} - {analytics ? ( -
-
- Session - {analytics.session_id} -
-
- Duration - {formatDuration(analytics.duration_ms)} -
-
- Puzzles solved - - {analytics.puzzles_solved} / {analytics.puzzles.length} - -
-
- Hints used - {analytics.total_hints} -
-
- Total attempts - {analytics.total_attempts} -
-
- ) : ( -

No analytics loaded. Press Refresh to fetch.

- )} -
- - {/* Last WS event */} - {runtime.lastEvent && ( -
-

Last event

-
-            {JSON.stringify(runtime.lastEvent, null, 2)}
-          
-
- )} - - {runtime.lastError && ( -

{runtime.lastError}

- )} - - {/* Spinner keyframe (injected once) */} - -
- ); -} diff --git a/frontend-scratch-v2/src/components/MediaManager.tsx b/frontend-scratch-v2/src/components/MediaManager.tsx deleted file mode 100644 index 0961a57..0000000 --- a/frontend-scratch-v2/src/components/MediaManager.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { - type MediaFileList, - type MediaStatus, - mediaFiles, - mediaPlay, - mediaStop, - mediaRecordStart, - mediaRecordStop, - legacyControl, -} from '../lib/api'; -import type { RuntimeState } from '../lib/useRuntimeStore'; -import { isMediaManagerActive } from '../lib/useRuntimeStore'; - -type Props = { runtime: RuntimeState }; - -type FileCategory = 'music' | 'picture' | 'recorder'; - -const CATEGORIES: FileCategory[] = ['music', 'picture', 'recorder']; - -export function MediaManager({ runtime }: Props) { - const [listings, setListings] = useState< - Record - >({ - music: null, - picture: null, - recorder: null, - }); - const [busy, setBusy] = useState(false); - const [feedback, setFeedback] = useState(''); - const [recSeconds, setRecSeconds] = useState(20); - const [recFilename, setRecFilename] = useState('take_1.wav'); - - const media: MediaStatus | undefined = runtime.legacy?.media; - const active = isMediaManagerActive(runtime.legacy); - - const loadFiles = useCallback(async () => { - const results = await Promise.allSettled( - CATEGORIES.map((kind) => mediaFiles(kind)), - ); - const next = { ...listings }; - CATEGORIES.forEach((kind, i) => { - const r = results[i]; - next[kind] = r.status === 'fulfilled' ? r.value : null; - }); - setListings(next); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (active) loadFiles(); - }, [active, loadFiles]); - - const run = async (label: string, fn: () => Promise) => { - setBusy(true); - setFeedback(''); - try { - await fn(); - setFeedback(`${label} OK`); - } catch (err) { - setFeedback( - `${label} failed: ${err instanceof Error ? err.message : err}`, - ); - } finally { - setBusy(false); - } - }; - - const handlePlay = (path: string) => - run('Play', async () => { - if (media?.playing) await mediaStop(); - await mediaPlay(path); - }); - - const handleStop = () => run('Stop', mediaStop); - - const handleRecordStart = () => - run('Record', () => mediaRecordStart(recSeconds, recFilename)); - - const handleRecordStop = () => run('RecStop', mediaRecordStop); - - return ( -
-

Media Manager

- - {!active && ( -
- Media Manager not active on device. Current step:{' '} - - {runtime.legacy?.story.screen ?? runtime.legacy?.story.step ?? '?'} - -
- )} - - {/* Media status */} - {media && ( -
-

Status

-
- Ready - - {String(media.ready)} - -
-
- Playing - - {String(media.playing)} - -
-
- Recording - - {String(media.recording)} - -
- {media.recording && ( -
- Elapsed - - {media.record_elapsed_seconds}s / {media.record_limit_seconds}s - -
- )} - {media.record_simulated && ( -
- Recording is simulated (placeholder WAV). -
- )} - {media.last_error && ( -
- Last error: {media.last_error} -
- )} -
- )} - - {/* File listings */} - {CATEGORIES.map((kind) => { - const list = listings[kind]; - return ( -
-

{kind.charAt(0).toUpperCase() + kind.slice(1)}

- {!list ? ( -

- Not loaded.{' '} - -

- ) : list.files.length === 0 ? ( -

No files.

- ) : ( -
    - {list.files.map((f) => ( -
  • - {f} - {kind === 'music' && ( - - )} -
  • - ))} -
- )} -
- ); - })} - - {/* Playback controls */} -
-

Playback

-
- - -
-
- - {/* Recording */} -
-

Recording

-
- - -
-
- - -
-
- - {/* Boot mode */} -
-

Boot mode

-
- - - - -
-
- - {feedback &&

{feedback}

} -
- ); -} diff --git a/frontend-scratch-v2/src/components/NetworkPanel.tsx b/frontend-scratch-v2/src/components/NetworkPanel.tsx deleted file mode 100644 index eef3612..0000000 --- a/frontend-scratch-v2/src/components/NetworkPanel.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useState } from 'react'; -import { networkReconnect, espnowOn, espnowOff } from '../lib/api'; -import type { RuntimeState } from '../lib/useRuntimeStore'; - -type Props = { runtime: RuntimeState }; - -export function NetworkPanel({ runtime }: Props) { - const [busy, setBusy] = useState(false); - const [feedback, setFeedback] = useState(''); - const net = runtime.legacy?.network; - - const run = async (label: string, fn: () => Promise) => { - setBusy(true); - setFeedback(''); - try { - await fn(); - setFeedback(`${label} OK`); - } catch (err) { - setFeedback( - `${label} failed: ${err instanceof Error ? err.message : err}`, - ); - } finally { - setBusy(false); - } - }; - - return ( -
-

Network

- -
-

WiFi

-
- State - {net?.state ?? '?'} -
-
- IP - {net?.ip ?? '?'} -
-
- -
-
- -
-

ESP-NOW

-
- - -
-
- - {feedback &&

{feedback}

} -
- ); -} diff --git a/frontend-scratch-v2/src/components/RuntimeControls.tsx b/frontend-scratch-v2/src/components/RuntimeControls.tsx deleted file mode 100644 index 20c2870..0000000 --- a/frontend-scratch-v2/src/components/RuntimeControls.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useState } from 'react'; - -const DEFAULT_BASE_URL = - (import.meta.env.VITE_STORY_API_BASE as string | undefined) ?? - 'http://localhost:8080'; - -type RuntimeControlsProps = { - yaml: string; -}; - -type ResponseView = { - label: string; - payload: string; -}; - -async function readResponsePayload(response: Response): Promise { - const text = await response.text(); - if (!text) { - return ''; - } - - try { - return JSON.stringify(JSON.parse(text), null, 2); - } catch { - return text; - } -} - -export function RuntimeControls({ yaml }: RuntimeControlsProps) { - const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL); - const [result, setResult] = useState({ - label: 'No request yet', - payload: '', - }); - const [busy, setBusy] = useState(false); - - const runRequest = async ( - label: string, - path: string, - init?: RequestInit, - ) => { - setBusy(true); - try { - const response = await fetch(`${baseUrl}${path}`, init); - const payload = await readResponsePayload(response); - setResult({ - label: `${label} -> HTTP ${response.status}`, - payload, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setResult({ - label: `${label} -> FAILED`, - payload: message, - }); - } finally { - setBusy(false); - } - }; - - return ( -
-
-
- setBaseUrl(event.target.value.trim())} - aria-label="story api base url" - placeholder="http://esp32:8080" - /> -
-
- - - - - - -
-
-
-        {result.label}
-        {'\n'}
-        {result.payload}
-      
-
- ); -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/ScenarioEditor.tsx b/frontend-scratch-v2/src/components/ScenarioEditor/ScenarioEditor.tsx deleted file mode 100644 index 3370ca3..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/ScenarioEditor.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useEffect, useRef, useState, useCallback, lazy, Suspense } from 'react'; -import * as Blockly from 'blockly'; -import 'blockly/blocks'; -import { ensureScenarioBlocks } from './blocks/scene'; -import { registerPuzzleBlocks } from './blocks/puzzle'; -import { registerNpcBlocks } from './blocks/npc'; -import { registerHardwareBlocks } from './blocks/hardware'; -import { registerDeployBlocks } from './blocks/deploy'; -import { SCENARIO_TOOLBOX } from './toolbox'; -import { - buildScenarioGraph, - scenarioGraphToFirmwareYaml, - scenarioGraphToDisplayYaml, -} from './generators/yaml'; -import { downloadYaml } from './export/download'; -import { encodeScenarioToUrl } from './export/share'; -import { validateScenarioGraph, formatValidationSummary } from './validators/workspace'; - -const LazyMonacoEditor = lazy(() => import('@monaco-editor/react')); - -type YamlMode = 'display' | 'firmware'; - -export function ScenarioEditor() { - const hostRef = useRef(null); - const workspaceRef = useRef(null); - const debounceRef = useRef | null>(null); - const [scenarioName, setScenarioName] = useState('new_scenario'); - const [yamlOutput, setYamlOutput] = useState(''); - const [yamlMode, setYamlMode] = useState('display'); - const [sceneCount, setSceneCount] = useState(0); - const [validationSummary, setValidationSummary] = useState(''); - const [validationErrors, setValidationErrors] = useState(0); - - const regenerateYaml = useCallback( - (workspace: Blockly.WorkspaceSvg, mode: YamlMode) => { - const graph = buildScenarioGraph(workspace); - setSceneCount(graph.scenes.length); - const issues = validateScenarioGraph(graph); - setValidationSummary(formatValidationSummary(graph, issues)); - setValidationErrors(issues.filter((i) => i.severity === 'error').length); - const yaml = - mode === 'firmware' - ? scenarioGraphToFirmwareYaml(graph) - : scenarioGraphToDisplayYaml(graph); - setYamlOutput(yaml); - }, - [], - ); - - useEffect(() => { - if (!hostRef.current) return; - - ensureScenarioBlocks(); - registerPuzzleBlocks(); - registerNpcBlocks(); - registerHardwareBlocks(); - registerDeployBlocks(); - const workspace = Blockly.inject(hostRef.current, { - toolbox: SCENARIO_TOOLBOX, - trashcan: true, - grid: { spacing: 20, length: 3, colour: '#3a3f52', snap: true }, - zoom: { controls: true, wheel: true, startScale: 0.95 }, - }); - workspaceRef.current = workspace; - - // Add a starter scene block - const block = workspace.newBlock('scenario_scene'); - block.setFieldValue('SCENE_INTRO', 'NAME'); - block.setFieldValue('Introduction scene', 'DESCRIPTION'); - block.initSvg(); - block.render(); - block.moveBy(40, 40); - - const onChange = (event: Blockly.Events.Abstract) => { - // Filter out UI-only events (viewport, click, etc.) - if (event.isUiEvent) return; - if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - regenerateYaml(workspace, yamlMode); - }, 150); - }; - - workspace.addChangeListener(onChange); - // Initial generation - regenerateYaml(workspace, yamlMode); - - return () => { - if (debounceRef.current) clearTimeout(debounceRef.current); - workspace.removeChangeListener(onChange); - workspace.dispose(); - workspaceRef.current = null; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Re-generate when mode changes - useEffect(() => { - if (workspaceRef.current) { - regenerateYaml(workspaceRef.current, yamlMode); - } - }, [yamlMode, regenerateYaml]); - - const handleExportDisplay = useCallback(() => { - const graph = workspaceRef.current ? buildScenarioGraph(workspaceRef.current) : null; - if (!graph) return; - const yaml = scenarioGraphToDisplayYaml(graph); - downloadYaml(yaml, `${scenarioName}.yaml`); - }, [scenarioName]); - - const handleExportFirmware = useCallback(() => { - const graph = workspaceRef.current ? buildScenarioGraph(workspaceRef.current) : null; - if (!graph) return; - const yaml = scenarioGraphToFirmwareYaml(graph); - downloadYaml(yaml, `${scenarioName}_firmware.yaml`); - }, [scenarioName]); - - const handleShare = useCallback(() => { - if (!workspaceRef.current) return; - const xml = Blockly.Xml.domToText( - Blockly.Xml.workspaceToDom(workspaceRef.current), - ); - const url = encodeScenarioToUrl(xml); - navigator.clipboard.writeText(url).then(() => { - // Visual feedback could be added here - }); - }, []); - - return ( -
-
- setScenarioName(e.target.value)} - aria-label="scenario name" - placeholder="scenario name" - /> - - - - - - {sceneCount} scene(s) - -
-
-
-
- - Loading editor... -
- } - > - - -
-
-
0 ? '#ff6b6b' : '#888', - backgroundColor: '#1a1a2e', - }} - data-testid="validation-status" - > - {validationSummary} -
-
- ); -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/deploy.ts b/frontend-scratch-v2/src/components/ScenarioEditor/blocks/deploy.ts deleted file mode 100644 index 9d037b4..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/deploy.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as Blockly from 'blockly'; - -let registered = false; - -export function registerDeployBlocks(): void { - if (registered) return; - - Blockly.defineBlocksWithJsonArray([ - { - type: 'deploy_config_wifi', - message0: 'WiFi SSID %1 password %2', - args0: [ - { type: 'field_input', name: 'SSID', text: '' }, - { type: 'field_input', name: 'PASSWORD', text: '' }, - ], - colour: 0, - tooltip: 'Configure WiFi credentials for ESP32 deployment', - }, - { - type: 'deploy_config_tts', - message0: 'TTS server %1 voice %2', - args0: [ - { type: 'field_input', name: 'URL', text: 'http://192.168.0.120:8001' }, - { - type: 'field_dropdown', - name: 'VOICE', - options: [ - ['tom-medium', 'tom-medium'], - ['siwis', 'siwis'], - ['upmc', 'upmc'], - ], - }, - ], - colour: 0, - tooltip: 'Configure Piper TTS server URL and voice for deployment', - }, - { - type: 'deploy_config_llm', - message0: 'LLM server %1 model %2', - args0: [ - { type: 'field_input', name: 'URL', text: 'http://kxkm-ai:11434' }, - { type: 'field_input', name: 'MODEL', text: 'devstral' }, - ], - colour: 0, - tooltip: 'Configure LLM server URL and model for deployment', - }, - { - type: 'deploy_export', - message0: 'Export pour ESP32', - colour: 0, - tooltip: 'Export the full scenario configuration for ESP32 firmware flashing', - }, - ]); - - registered = true; -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/hardware.ts b/frontend-scratch-v2/src/components/ScenarioEditor/blocks/hardware.ts deleted file mode 100644 index 2c079c7..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/hardware.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as Blockly from 'blockly'; - -let registered = false; - -export function registerHardwareBlocks(): void { - if (registered) return; - - Blockly.defineBlocksWithJsonArray([ - { - type: 'hw_gpio_write', - message0: 'GPIO write pin %1 state %2', - args0: [ - { type: 'field_number', name: 'PIN', value: 4, min: 0, max: 48, precision: 1 }, - { - type: 'field_dropdown', - name: 'STATE', - options: [ - ['HIGH', 'HIGH'], - ['LOW', 'LOW'], - ], - }, - ], - previousStatement: null, - nextStatement: null, - colour: 30, - tooltip: 'Write a digital value to a GPIO pin', - }, - { - type: 'hw_gpio_read', - message0: 'GPIO read pin %1 into %2', - args0: [ - { type: 'field_number', name: 'PIN', value: 4, min: 0, max: 48, precision: 1 }, - { type: 'field_input', name: 'VARIABLE', text: 'pin_value' }, - ], - previousStatement: null, - nextStatement: null, - colour: 30, - tooltip: 'Read a digital value from a GPIO pin into a variable', - }, - { - type: 'hw_led_set', - message0: 'LED color %1 animation %2', - args0: [ - { type: 'field_input', name: 'COLOR', text: '#00FF00' }, - { - type: 'field_dropdown', - name: 'ANIMATION', - options: [ - ['solid', 'solid'], - ['blink', 'blink'], - ['pulse', 'pulse'], - ['rainbow', 'rainbow'], - ], - }, - ], - previousStatement: null, - nextStatement: null, - colour: 30, - tooltip: 'Set LED color and animation pattern', - }, - { - type: 'hw_buzzer_tone', - message0: 'Buzzer %1 Hz for %2 ms', - args0: [ - { type: 'field_number', name: 'FREQUENCY', value: 440, min: 100, max: 5000, precision: 1 }, - { type: 'field_number', name: 'DURATION_MS', value: 500, min: 0, precision: 1 }, - ], - previousStatement: null, - nextStatement: null, - colour: 30, - tooltip: 'Play a tone on the buzzer at a given frequency and duration', - }, - { - type: 'hw_play_audio', - message0: 'Play audio %1', - args0: [ - { type: 'field_input', name: 'FILENAME', text: 'audio.mp3' }, - ], - previousStatement: null, - nextStatement: null, - colour: 30, - tooltip: 'Play an audio file from the SD card', - }, - { - type: 'hw_qr_scan', - message0: 'QR scan', - previousStatement: null, - nextStatement: null, - colour: 30, - tooltip: 'Activate the QR code scanner and wait for a scan result', - }, - ]); - - registered = true; -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/npc.ts b/frontend-scratch-v2/src/components/ScenarioEditor/blocks/npc.ts deleted file mode 100644 index 6d9669f..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/npc.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as Blockly from 'blockly'; - -let registered = false; - -const MOOD_OPTIONS: Array<[string, string]> = [ - ['neutral', 'neutral'], - ['impressed', 'impressed'], - ['worried', 'worried'], - ['amused', 'amused'], -]; - -export function registerNpcBlocks(): void { - if (registered) return; - - Blockly.defineBlocksWithJsonArray([ - { - type: 'npc_say', - message0: 'NPC says %1 mood %2', - args0: [ - { type: 'field_input', name: 'TEXT', text: '' }, - { - type: 'field_dropdown', - name: 'MOOD', - options: MOOD_OPTIONS, - }, - ], - previousStatement: null, - nextStatement: null, - colour: 160, - tooltip: 'Professor Zacus says something with a mood', - }, - { - type: 'npc_mood_set', - message0: 'set NPC mood %1', - args0: [ - { - type: 'field_dropdown', - name: 'MOOD', - options: MOOD_OPTIONS, - }, - ], - previousStatement: null, - nextStatement: null, - colour: 160, - tooltip: 'Change the NPC mood state', - }, - { - type: 'npc_hint', - message0: 'hint level %1 for puzzle %2 text %3', - args0: [ - { - type: 'field_dropdown', - name: 'LEVEL', - options: [ - ['1', '1'], - ['2', '2'], - ['3', '3'], - ], - }, - { type: 'field_input', name: 'PUZZLE_ID', text: '' }, - { type: 'field_input', name: 'TEXT', text: '' }, - ], - previousStatement: null, - nextStatement: null, - colour: 160, - tooltip: 'A hint for a specific puzzle at a given difficulty level', - }, - { - type: 'npc_react', - message0: 'NPC reacts to %1 with %2', - args0: [ - { type: 'input_value', name: 'CONDITION' }, - { type: 'field_input', name: 'RESPONSE', text: '' }, - ], - previousStatement: null, - nextStatement: null, - colour: 160, - tooltip: 'NPC reacts when a condition is met', - }, - { - type: 'npc_conversation', - message0: 'conversation prompt %1 context %2', - args0: [ - { type: 'field_input', name: 'SYSTEM_PROMPT', text: '' }, - { type: 'field_input', name: 'CONTEXT', text: '' }, - ], - output: null, - colour: 160, - tooltip: 'An LLM-powered NPC conversation with system prompt and context', - }, - ]); - - registered = true; -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/puzzle.ts b/frontend-scratch-v2/src/components/ScenarioEditor/blocks/puzzle.ts deleted file mode 100644 index 1cea7ad..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/puzzle.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as Blockly from 'blockly'; - -let registered = false; - -export function registerPuzzleBlocks(): void { - if (registered) return; - - Blockly.defineBlocksWithJsonArray([ - { - type: 'puzzle_definition', - message0: 'Puzzle %1 type %2', - args0: [ - { type: 'field_input', name: 'NAME', text: 'PUZZLE_NEW' }, - { - type: 'field_dropdown', - name: 'PUZZLE_TYPE', - options: [ - ['QR', 'qr'], - ['Button', 'button'], - ['Sequence', 'sequence'], - ['Free', 'free'], - ], - }, - ], - message1: 'solution %1', - args1: [{ type: 'input_value', name: 'SOLUTION' }], - message2: 'hints %1', - args2: [{ type: 'input_statement', name: 'HINTS' }], - colour: 210, - tooltip: 'Define a puzzle with a type, solution, and hints', - }, - { - type: 'puzzle_condition', - message0: '%1 %2', - args0: [ - { - type: 'field_dropdown', - name: 'CONDITION_TYPE', - options: [ - ['puzzle solved', 'puzzle_solved'], - ['timer expired', 'timer_expired'], - ['variable equals', 'variable_equals'], - ], - }, - { type: 'field_input', name: 'REFERENCE', text: '' }, - ], - output: 'Boolean', - colour: 210, - tooltip: 'A condition that evaluates to true/false', - }, - { - type: 'puzzle_validation_qr', - message0: 'QR expected %1', - args0: [ - { type: 'field_input', name: 'EXPECTED', text: 'ZACUS_KEY_1' }, - ], - output: null, - colour: 210, - tooltip: 'QR code validation — matches scanned value against expected', - }, - { - type: 'puzzle_validation_button', - message0: 'Button pin %1', - args0: [ - { type: 'field_number', name: 'PIN', value: 4, min: 0, precision: 1 }, - ], - output: null, - colour: 210, - tooltip: 'Button press validation on a specific GPIO pin', - }, - ]); - - registered = true; -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/scene.ts b/frontend-scratch-v2/src/components/ScenarioEditor/blocks/scene.ts deleted file mode 100644 index ef5235a..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/blocks/scene.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as Blockly from 'blockly'; - -let registered = false; - -export function ensureScenarioBlocks(): void { - if (registered) return; - - Blockly.defineBlocksWithJsonArray([ - { - type: 'scenario_scene', - message0: 'Scene %1 description %2', - args0: [ - { type: 'field_input', name: 'NAME', text: 'SCENE_NEW' }, - { type: 'field_input', name: 'DESCRIPTION', text: '' }, - ], - message1: 'duration max %1 s', - args1: [ - { type: 'field_number', name: 'DURATION_MAX', value: 300, min: 0, precision: 1 }, - ], - message2: 'actions %1', - args2: [{ type: 'input_statement', name: 'ACTIONS' }], - message3: 'transitions %1', - args3: [{ type: 'input_statement', name: 'TRANSITIONS' }], - colour: 270, - tooltip: 'A scenario scene with actions and transitions', - }, - { - type: 'scenario_transition', - message0: 'go to %1 when %2', - args0: [ - { type: 'field_input', name: 'TARGET_SCENE', text: 'SCENE_NEXT' }, - { type: 'input_value', name: 'CONDITION' }, - ], - previousStatement: null, - nextStatement: null, - colour: 160, - tooltip: 'Transition to another scene when condition is met', - }, - { - type: 'scenario_timer', - message0: 'timer %1 s', - args0: [ - { type: 'field_number', name: 'SECONDS', value: 10, min: 0, precision: 1 }, - ], - message1: 'on expire %1', - args1: [{ type: 'input_statement', name: 'ON_EXPIRE' }], - previousStatement: null, - nextStatement: null, - colour: 60, - tooltip: 'Wait for a duration then execute actions', - }, - { - type: 'scenario_variable_set', - message0: 'set %1 to %2', - args0: [ - { type: 'field_input', name: 'NAME', text: 'my_var' }, - { type: 'input_value', name: 'VALUE' }, - ], - previousStatement: null, - nextStatement: null, - colour: 330, - tooltip: 'Set a scenario variable', - }, - { - type: 'scenario_variable_get', - message0: 'get %1', - args0: [ - { type: 'field_input', name: 'NAME', text: 'my_var' }, - ], - output: null, - colour: 330, - tooltip: 'Get a scenario variable value', - }, - ]); - - registered = true; -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/export/download.ts b/frontend-scratch-v2/src/components/ScenarioEditor/export/download.ts deleted file mode 100644 index 4e85a6f..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/export/download.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function downloadYaml(yaml: string, filename: string): void { - const blob = new Blob([yaml], { type: 'text/yaml' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); -} - -export function downloadJson(data: object, filename: string): void { - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/export/share.ts b/frontend-scratch-v2/src/components/ScenarioEditor/export/share.ts deleted file mode 100644 index 8027748..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/export/share.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function encodeScenarioToUrl(workspaceXml: string): string { - const compressed = btoa(encodeURIComponent(workspaceXml)); - return `${window.location.origin}${window.location.pathname}#scenario=${compressed}`; -} - -export function decodeScenarioFromUrl(hash: string): string | null { - const match = hash.match(/#scenario=(.+)/); - if (!match) return null; - try { - return decodeURIComponent(atob(match[1])); - } catch { - return null; - } -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/generators/yaml.ts b/frontend-scratch-v2/src/components/ScenarioEditor/generators/yaml.ts deleted file mode 100644 index afbdc30..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/generators/yaml.ts +++ /dev/null @@ -1,449 +0,0 @@ -import * as Blockly from 'blockly'; -import YAML from 'yaml'; -import type { - ScenarioGraph, - SceneNode, - TransitionEdge, - SceneAction, - TimerAction, - VariableSetAction, - PuzzleNode, - NPCAction, - HardwareAction, - DeployConfig, -} from '../types'; - -// ─── Workspace → ScenarioGraph ─── - -function readConditionText(block: Blockly.Block, inputName: string): string { - const target = block.getInputTargetBlock(inputName); - if (!target) return ''; - if (target.type === 'scenario_variable_get') { - return `var:${target.getFieldValue('NAME') ?? 'unknown'}`; - } - return target.type; -} - -function readActions(block: Blockly.Block, inputName: string): SceneAction[] { - const actions: SceneAction[] = []; - let cursor = block.getInputTargetBlock(inputName); - while (cursor) { - if (cursor.type === 'scenario_timer') { - const timerAction: TimerAction = { - kind: 'timer', - seconds: Number(cursor.getFieldValue('SECONDS') ?? 10), - onExpire: readActions(cursor, 'ON_EXPIRE'), - }; - actions.push(timerAction); - } else if (cursor.type === 'scenario_variable_set') { - const varAction: VariableSetAction = { - kind: 'variable_set', - name: String(cursor.getFieldValue('NAME') ?? 'my_var'), - value: readConditionText(cursor, 'VALUE'), - }; - actions.push(varAction); - } - cursor = cursor.getNextBlock(); - } - return actions; -} - -function readTransitions(block: Blockly.Block): TransitionEdge[] { - const transitions: TransitionEdge[] = []; - let cursor = block.getInputTargetBlock('TRANSITIONS'); - while (cursor) { - if (cursor.type === 'scenario_transition') { - transitions.push({ - targetScene: String(cursor.getFieldValue('TARGET_SCENE') ?? 'SCENE_NEXT'), - condition: readConditionText(cursor, 'CONDITION'), - }); - } - cursor = cursor.getNextBlock(); - } - return transitions; -} - -// ─── Puzzle blocks → PuzzleNode ─── - -function readPuzzleHints(block: Blockly.Block): Array<{ level: number; text: string }> { - const hints: Array<{ level: number; text: string }> = []; - let cursor = block.getInputTargetBlock('HINTS'); - while (cursor) { - if (cursor.type === 'npc_hint') { - hints.push({ - level: Number(cursor.getFieldValue('LEVEL') ?? 1), - text: String(cursor.getFieldValue('TEXT') ?? ''), - }); - } - cursor = cursor.getNextBlock(); - } - return hints; -} - -function readPuzzleSolution(block: Blockly.Block): string | undefined { - const target = block.getInputTargetBlock('SOLUTION'); - if (!target) return undefined; - if (target.type === 'puzzle_validation_qr') { - return `qr:${target.getFieldValue('EXPECTED') ?? ''}`; - } - if (target.type === 'puzzle_validation_button') { - return `button:${target.getFieldValue('PIN') ?? 4}`; - } - return target.type; -} - -// ─── NPC blocks → NPCAction[] ─── - -function readNpcActions(workspace: Blockly.Workspace): NPCAction[] { - const actions: NPCAction[] = []; - for (const block of workspace.getAllBlocks(true)) { - switch (block.type) { - case 'npc_say': - actions.push({ - type: 'say', - text: String(block.getFieldValue('TEXT') ?? ''), - mood: String(block.getFieldValue('MOOD') ?? 'neutral'), - }); - break; - case 'npc_mood_set': - actions.push({ - type: 'mood', - mood: String(block.getFieldValue('MOOD') ?? 'neutral'), - }); - break; - case 'npc_hint': - // Only collect standalone hints (not those inside puzzle_definition) - if (!block.getParent() || block.getParent()?.type !== 'puzzle_definition') { - actions.push({ - type: 'hint', - level: Number(block.getFieldValue('LEVEL') ?? 1), - text: String(block.getFieldValue('TEXT') ?? ''), - puzzleId: String(block.getFieldValue('PUZZLE_ID') ?? ''), - }); - } - break; - case 'npc_react': - actions.push({ - type: 'react', - condition: readConditionText(block, 'CONDITION'), - text: String(block.getFieldValue('RESPONSE') ?? ''), - }); - break; - case 'npc_conversation': - actions.push({ - type: 'conversation', - systemPrompt: String(block.getFieldValue('SYSTEM_PROMPT') ?? ''), - text: String(block.getFieldValue('CONTEXT') ?? ''), - }); - break; - } - } - return actions; -} - -// ─── Hardware blocks → HardwareAction[] ─── - -function readHardwareActions(workspace: Blockly.Workspace): HardwareAction[] { - const actions: HardwareAction[] = []; - for (const block of workspace.getAllBlocks(true)) { - switch (block.type) { - case 'hw_gpio_write': - actions.push({ - type: 'gpio_write', - pin: Number(block.getFieldValue('PIN') ?? 4), - state: String(block.getFieldValue('STATE') ?? 'HIGH') as 'HIGH' | 'LOW', - }); - break; - case 'hw_gpio_read': - actions.push({ - type: 'gpio_read', - pin: Number(block.getFieldValue('PIN') ?? 4), - variable: String(block.getFieldValue('VARIABLE') ?? 'pin_value'), - }); - break; - case 'hw_led_set': - actions.push({ - type: 'led_set', - color: String(block.getFieldValue('COLOR') ?? '#00FF00'), - animation: String(block.getFieldValue('ANIMATION') ?? 'solid'), - }); - break; - case 'hw_buzzer_tone': - actions.push({ - type: 'buzzer', - frequency: Number(block.getFieldValue('FREQUENCY') ?? 440), - duration_ms: Number(block.getFieldValue('DURATION_MS') ?? 500), - }); - break; - case 'hw_play_audio': - actions.push({ - type: 'play_audio', - filename: String(block.getFieldValue('FILENAME') ?? 'audio.mp3'), - }); - break; - case 'hw_qr_scan': - actions.push({ type: 'qr_scan' }); - break; - } - } - return actions; -} - -// ─── Deploy blocks → DeployConfig ─── - -function readDeployConfig(workspace: Blockly.Workspace): DeployConfig { - const config: DeployConfig = {}; - for (const block of workspace.getAllBlocks(true)) { - switch (block.type) { - case 'deploy_config_wifi': - config.wifi = { - ssid: String(block.getFieldValue('SSID') ?? ''), - password: String(block.getFieldValue('PASSWORD') ?? ''), - }; - break; - case 'deploy_config_tts': - config.tts = { - url: String(block.getFieldValue('URL') ?? 'http://192.168.0.120:8001'), - voice: String(block.getFieldValue('VOICE') ?? 'tom-medium'), - }; - break; - case 'deploy_config_llm': - config.llm = { - url: String(block.getFieldValue('URL') ?? 'http://kxkm-ai:11434'), - model: String(block.getFieldValue('MODEL') ?? 'devstral'), - }; - break; - } - } - return config; -} - -/** - * Walk workspace top blocks and build a ScenarioGraph. - * Works with both WorkspaceSvg and headless Workspace. - */ -export function buildScenarioGraph(workspace: Blockly.Workspace): ScenarioGraph { - const scenes: SceneNode[] = []; - const puzzles: PuzzleNode[] = []; - const seen = new Set(); - - for (const block of workspace.getTopBlocks(true)) { - if (block.type === 'scenario_scene') { - const id = block.id; - if (seen.has(id)) continue; - seen.add(id); - - scenes.push({ - name: String(block.getFieldValue('NAME') ?? 'SCENE_NEW'), - description: String(block.getFieldValue('DESCRIPTION') ?? ''), - durationMax: Number(block.getFieldValue('DURATION_MAX') ?? 300), - actions: readActions(block, 'ACTIONS'), - transitions: readTransitions(block), - }); - } else if (block.type === 'puzzle_definition') { - const id = block.id; - if (seen.has(id)) continue; - seen.add(id); - - puzzles.push({ - id, - name: String(block.getFieldValue('NAME') ?? 'PUZZLE_NEW'), - type: String(block.getFieldValue('PUZZLE_TYPE') ?? 'free') as PuzzleNode['type'], - solution: readPuzzleSolution(block), - hints: readPuzzleHints(block), - }); - } - } - - const npcActions = readNpcActions(workspace); - const hardwareActions = readHardwareActions(workspace); - const deploy = readDeployConfig(workspace); - - return { scenes, puzzles, npcActions, hardwareActions, deploy }; -} - -// ─── ScenarioGraph → Firmware YAML ─── - -function actionsToFirmwareTransitions( - actions: SceneAction[], - sceneIndex: number, -): Array<{ - event_type: string; - event_name: string; - target_step_id: string; - priority: number; - after_ms: number; -}> { - const result: Array<{ - event_type: string; - event_name: string; - target_step_id: string; - priority: number; - after_ms: number; - }> = []; - for (const action of actions) { - if (action.kind === 'timer') { - // A timer with on_expire transitions generates a timer-type firmware transition - for (const child of action.onExpire) { - if (child.kind === 'variable_set') { - // Variable sets in on_expire map to action-type transitions - result.push({ - event_type: 'action', - event_name: `SET_${child.name.toUpperCase()}`, - target_step_id: '', - priority: 0, - after_ms: action.seconds * 1000, - }); - } - } - } - } - void sceneIndex; - return result; -} - -/** - * Project ScenarioGraph to firmware.steps[] format compatible with compile_runtime3.py - */ -export function scenarioGraphToFirmwareYaml(graph: ScenarioGraph): string { - const steps = graph.scenes.map((scene, index) => { - const stepId = scene.name.toUpperCase().replace(/[^A-Z0-9_]/g, '_'); - - // Build transitions from scene transitions + timer actions - const transitions = scene.transitions.map((t, ti) => ({ - event_type: t.condition ? 'serial' : 'button', - event_name: t.condition || `TR_${ti + 1}`, - target_step_id: t.targetScene.toUpperCase().replace(/[^A-Z0-9_]/g, '_'), - priority: 0, - after_ms: 0, - })); - - // Add timer-derived transitions - transitions.push(...actionsToFirmwareTransitions(scene.actions, index)); - - return { - step_id: stepId, - screen_scene_id: stepId, - audio_pack_id: '', - actions: [] as Record[], - apps: [] as string[], - transitions, - }; - }); - - // Map puzzles to firmware events - const puzzleEvents = graph.puzzles.map((p) => ({ - puzzle_id: p.name.toUpperCase().replace(/[^A-Z0-9_]/g, '_'), - type: p.type, - solution: p.solution ?? '', - hints_count: p.hints.length, - })); - - // Map hardware actions to firmware action entries - const hwActions = graph.hardwareActions.map((a) => { - const entry: Record = { type: a.type }; - if (a.pin !== undefined) entry.pin = a.pin; - if (a.state !== undefined) entry.state = a.state; - if (a.variable !== undefined) entry.variable = a.variable; - if (a.color !== undefined) entry.color = a.color; - if (a.animation !== undefined) entry.animation = a.animation; - if (a.frequency !== undefined) entry.frequency = a.frequency; - if (a.duration_ms !== undefined) entry.duration_ms = a.duration_ms; - if (a.filename !== undefined) entry.filename = a.filename; - return entry; - }); - - // Attach hardware actions to the first step (default assignment) - if (steps.length > 0 && hwActions.length > 0) { - steps[0].actions = hwActions; - } - - const deploy = graph.deploy; - const hasDeployConfig = - deploy.wifi !== undefined || deploy.tts !== undefined || deploy.llm !== undefined; - - const firmware: Record = { - initial_step: steps[0]?.step_id ?? 'STEP_BOOT', - steps, - ...(puzzleEvents.length > 0 ? { puzzles: puzzleEvents } : {}), - ...(hasDeployConfig ? { deploy } : {}), - }; - - return YAML.stringify({ firmware }); -} - -// ─── ScenarioGraph → Display YAML ─── - -function actionToDisplay(action: SceneAction): Record { - if (action.kind === 'timer') { - return { - type: 'timer', - seconds: action.seconds, - on_expire: action.onExpire.map(actionToDisplay), - }; - } - return { - type: 'variable_set', - name: action.name, - value: action.value, - }; -} - -/** - * Project ScenarioGraph to a rich human-readable YAML format - */ -export function scenarioGraphToDisplayYaml(graph: ScenarioGraph): string { - const scenes = graph.scenes.map((scene) => ({ - name: scene.name, - description: scene.description, - duration_max_s: scene.durationMax, - actions: scene.actions.map(actionToDisplay), - transitions: scene.transitions.map((t) => ({ - target: t.targetScene, - condition: t.condition || undefined, - })), - })); - - const puzzles = graph.puzzles.map((p) => ({ - name: p.name, - type: p.type, - solution: p.solution ?? undefined, - hints: p.hints.map((h) => ({ level: h.level, text: h.text })), - })); - - const npc = graph.npcActions.map((a) => { - const entry: Record = { type: a.type }; - if (a.text !== undefined) entry.text = a.text; - if (a.mood !== undefined) entry.mood = a.mood; - if (a.level !== undefined) entry.level = a.level; - if (a.puzzleId !== undefined) entry.puzzle_id = a.puzzleId; - if (a.condition) entry.condition = a.condition; - if (a.systemPrompt) entry.system_prompt = a.systemPrompt; - return entry; - }); - - const hardware = graph.hardwareActions.map((a) => { - const entry: Record = { type: a.type }; - if (a.pin !== undefined) entry.pin = a.pin; - if (a.state !== undefined) entry.state = a.state; - if (a.variable !== undefined) entry.variable = a.variable; - if (a.color !== undefined) entry.color = a.color; - if (a.animation !== undefined) entry.animation = a.animation; - if (a.frequency !== undefined) entry.frequency = a.frequency; - if (a.duration_ms !== undefined) entry.duration_ms = a.duration_ms; - if (a.filename !== undefined) entry.filename = a.filename; - return entry; - }); - - const deploy = graph.deploy; - const hasDeployConfig = - deploy.wifi !== undefined || deploy.tts !== undefined || deploy.llm !== undefined; - - const doc: Record = { scenes }; - if (puzzles.length > 0) doc.puzzles = puzzles; - if (npc.length > 0) doc.npc = npc; - if (hardware.length > 0) doc.hardware = hardware; - if (hasDeployConfig) doc.deploy = deploy; - - return YAML.stringify(doc); -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/index.ts b/frontend-scratch-v2/src/components/ScenarioEditor/index.ts deleted file mode 100644 index 95884c8..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ScenarioEditor } from './ScenarioEditor'; diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/toolbox.ts b/frontend-scratch-v2/src/components/ScenarioEditor/toolbox.ts deleted file mode 100644 index 6b0e067..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/toolbox.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type * as Blockly from 'blockly'; - -export const SCENARIO_TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = { - kind: 'categoryToolbox', - contents: [ - { - kind: 'category', - name: 'Scenario', - colour: '270', - contents: [ - { kind: 'block', type: 'scenario_scene' }, - { kind: 'block', type: 'scenario_transition' }, - { kind: 'block', type: 'scenario_timer' }, - { kind: 'block', type: 'scenario_variable_set' }, - { kind: 'block', type: 'scenario_variable_get' }, - ], - }, - { - kind: 'category', - name: 'Puzzles', - colour: '210', - contents: [ - { kind: 'block', type: 'puzzle_definition' }, - { kind: 'block', type: 'puzzle_condition' }, - { kind: 'block', type: 'puzzle_validation_qr' }, - { kind: 'block', type: 'puzzle_validation_button' }, - ], - }, - { - kind: 'category', - name: 'NPC / Dialogue', - colour: '160', - contents: [ - { kind: 'block', type: 'npc_say' }, - { kind: 'block', type: 'npc_mood_set' }, - { kind: 'block', type: 'npc_hint' }, - { kind: 'block', type: 'npc_react' }, - { kind: 'block', type: 'npc_conversation' }, - ], - }, - { - kind: 'category', - name: 'Hardware', - colour: '30', - contents: [ - { kind: 'block', type: 'hw_gpio_write' }, - { kind: 'block', type: 'hw_gpio_read' }, - { kind: 'block', type: 'hw_led_set' }, - { kind: 'block', type: 'hw_buzzer_tone' }, - { kind: 'block', type: 'hw_play_audio' }, - { kind: 'block', type: 'hw_qr_scan' }, - ], - }, - { - kind: 'category', - name: 'Deploy', - colour: '0', - contents: [ - { kind: 'block', type: 'deploy_config_wifi' }, - { kind: 'block', type: 'deploy_config_tts' }, - { kind: 'block', type: 'deploy_config_llm' }, - { kind: 'block', type: 'deploy_export' }, - ], - }, - ], -}; diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/types.ts b/frontend-scratch-v2/src/components/ScenarioEditor/types.ts deleted file mode 100644 index d05d25a..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/types.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** A scene in the scenario graph */ -export interface SceneNode { - /** Unique scene identifier (e.g. SCENE_INTRO) */ - name: string; - /** Human-readable description */ - description: string; - /** Maximum duration in seconds (0 = unlimited) */ - durationMax: number; - /** Actions attached to this scene (timers, variable sets, etc.) */ - actions: SceneAction[]; - /** Outgoing transitions */ - transitions: TransitionEdge[]; -} - -/** A transition from one scene to another */ -export interface TransitionEdge { - /** Target scene name */ - targetScene: string; - /** Optional condition description (from connected value block) */ - condition: string; -} - -/** A timer action inside a scene */ -export interface TimerAction { - kind: 'timer'; - seconds: number; - /** Actions to execute on expiry (nested transitions, variable sets, etc.) */ - onExpire: SceneAction[]; -} - -/** A variable set action */ -export interface VariableSetAction { - kind: 'variable_set'; - name: string; - value: string; -} - -export type SceneAction = TimerAction | VariableSetAction; - -/** A puzzle definition in the scenario */ -export interface PuzzleNode { - id: string; - name: string; - type: 'qr' | 'button' | 'sequence' | 'free'; - solution?: string; - hints: Array<{ level: number; text: string }>; -} - -/** An NPC action (say, mood change, hint, react, conversation) */ -export interface NPCAction { - type: 'say' | 'mood' | 'hint' | 'react' | 'conversation'; - text?: string; - mood?: string; - level?: number; - puzzleId?: string; - condition?: string; - systemPrompt?: string; -} - -/** A hardware action (GPIO, LED, buzzer, audio, QR) */ -export interface HardwareAction { - type: 'gpio_write' | 'gpio_read' | 'led_set' | 'buzzer' | 'play_audio' | 'qr_scan'; - pin?: number; - state?: 'HIGH' | 'LOW'; - variable?: string; - color?: string; - animation?: string; - frequency?: number; - duration_ms?: number; - filename?: string; -} - -/** Deploy configuration for ESP32 targets */ -export interface DeployConfig { - wifi?: { ssid: string; password: string }; - tts?: { url: string; voice: string }; - llm?: { url: string; model: string }; -} - -/** The full scenario graph extracted from the Blockly workspace */ -export interface ScenarioGraph { - scenes: SceneNode[]; - puzzles: PuzzleNode[]; - npcActions: NPCAction[]; - hardwareActions: HardwareAction[]; - deploy: DeployConfig; -} diff --git a/frontend-scratch-v2/src/components/ScenarioEditor/validators/workspace.ts b/frontend-scratch-v2/src/components/ScenarioEditor/validators/workspace.ts deleted file mode 100644 index 27d8bb6..0000000 --- a/frontend-scratch-v2/src/components/ScenarioEditor/validators/workspace.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { ScenarioGraph } from '../types'; - -export interface ValidationIssue { - severity: 'error' | 'warning'; - message: string; - blockId?: string; -} - -export function validateScenarioGraph(graph: ScenarioGraph): ValidationIssue[] { - const issues: ValidationIssue[] = []; - - // Check: at least one scene - if (graph.scenes.length === 0) { - issues.push({ severity: 'error', message: 'No scenes defined' }); - } - - // Check: duplicate scene names - const sceneNames = graph.scenes.map((s) => s.name); - const dupes = sceneNames.filter((name, i) => sceneNames.indexOf(name) !== i); - for (const d of new Set(dupes)) { - issues.push({ severity: 'error', message: `Duplicate scene ID: "${d}"` }); - } - - // Check: transitions point to existing scenes - for (const scene of graph.scenes) { - for (const t of scene.transitions) { - if (!sceneNames.includes(t.targetScene)) { - issues.push({ - severity: 'error', - message: `Transition target "${t.targetScene}" not found`, - blockId: scene.name, - }); - } - } - } - - // Check: unreachable scenes (no incoming transitions, except first) - if (graph.scenes.length > 1) { - const targets = new Set( - graph.scenes.flatMap((s) => s.transitions.map((t) => t.targetScene)), - ); - for (const scene of graph.scenes.slice(1)) { - if (!targets.has(scene.name)) { - issues.push({ - severity: 'warning', - message: `Scene "${scene.name}" is unreachable (no incoming transitions)`, - blockId: scene.name, - }); - } - } - } - - // Check: puzzles without hints - for (const puzzle of graph.puzzles) { - if (puzzle.hints.length === 0) { - issues.push({ - severity: 'warning', - message: `Puzzle "${puzzle.name}" has no hints`, - }); - } - } - - // Check: duplicate puzzle IDs - const puzzleIds = graph.puzzles.map((p) => p.id); - const puzzleDupes = puzzleIds.filter((id, i) => puzzleIds.indexOf(id) !== i); - for (const d of new Set(puzzleDupes)) { - issues.push({ severity: 'error', message: `Duplicate puzzle ID: "${d}"` }); - } - - return issues; -} - -export function formatValidationSummary( - graph: ScenarioGraph, - issues: ValidationIssue[], -): string { - const errors = issues.filter((i) => i.severity === 'error').length; - const warnings = issues.filter((i) => i.severity === 'warning').length; - const stats = `${graph.scenes.length} scenes, ${graph.puzzles.length} puzzles, ${graph.npcActions.length} NPC lines`; - - if (errors > 0) { - return `${stats} | ${errors} error${errors > 1 ? 's' : ''}, ${warnings} warning${warnings > 1 ? 's' : ''}`; - } - if (warnings > 0) { - return `${stats} | ${warnings} warning${warnings > 1 ? 's' : ''}`; - } - return `${stats} | Ready`; -} diff --git a/frontend-scratch-v2/src/index.css b/frontend-scratch-v2/src/index.css deleted file mode 100644 index 267833c..0000000 --- a/frontend-scratch-v2/src/index.css +++ /dev/null @@ -1,15 +0,0 @@ -:root { - font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, - Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - margin: 0; - min-width: 320px; -} diff --git a/frontend-scratch-v2/src/lib/api.ts b/frontend-scratch-v2/src/lib/api.ts deleted file mode 100644 index cb24b40..0000000 --- a/frontend-scratch-v2/src/lib/api.ts +++ /dev/null @@ -1,407 +0,0 @@ -/** - * API client aligned with STORY_RUNTIME_API_JSON_CONTRACT.md - * Supports Story V2 + Legacy Freenove endpoints. - */ - -const DEFAULT_BASE = - (import.meta.env.VITE_STORY_API_BASE as string | undefined) ?? - 'http://localhost:8080'; - -let baseUrl = DEFAULT_BASE; - -export function setApiBase(url: string) { - baseUrl = url.replace(/\/+$/, ''); -} -export function getApiBase() { - return baseUrl; -} - -// ─── Helpers ─── - -async function jsonFetch(path: string, init?: RequestInit, timeoutMs = 5000): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - let res: Response; - try { - res = await fetch(`${baseUrl}${path}`, { - ...init, - signal: controller.signal, - }); - } catch (err) { - clearTimeout(timeoutId); - if (err instanceof DOMException && err.name === 'AbortError') { - throw new ApiError(0, `Request timed out after ${timeoutMs}ms`, path); - } - throw err; - } - clearTimeout(timeoutId); - const text = await res.text(); - if (!text) throw new ApiError(res.status, 'empty response', path); - const data = JSON.parse(text); - if (!res.ok) { - const msg = data?.error?.message ?? data?.error ?? res.statusText; - throw new ApiError(res.status, msg, path); - } - if (data?.ok === false) { - throw new ApiError(res.status, data.error ?? 'operation failed', path); - } - return data as T; -} - -export class ApiError extends Error { - status: number; - path: string; - - constructor( - status: number, - message: string, - path: string, - ) { - super(message); - this.name = 'ApiError'; - this.status = status; - this.path = path; - } -} - -// ─── Story V2 ─── - -export interface ScenarioListItem { - id: string; - version: number; - estimated_duration_s: number; -} - -export interface StoryStatus { - status: 'idle' | 'running' | 'paused'; - scenario_id: string; - current_step: string; - progress_pct: number; - started_at_ms: number; - selected: string; - queue_depth: number; -} - -export async function storyList(): Promise { - const data = await jsonFetch<{ scenarios: ScenarioListItem[] }>( - '/api/story/list', - ); - return data.scenarios; -} - -export async function storyStatus(): Promise { - return jsonFetch('/api/story/status'); -} - -export async function storySelect(scenarioId: string) { - return jsonFetch<{ selected: string; status: string }>( - `/api/story/select/${encodeURIComponent(scenarioId)}`, - { method: 'POST' }, - ); -} - -export async function storyStart() { - return jsonFetch<{ status: string; current_step: string }>( - '/api/story/start', - { method: 'POST' }, - ); -} - -export async function storyPause() { - return jsonFetch<{ status: string }>('/api/story/pause', { method: 'POST' }); -} - -export async function storyResume() { - return jsonFetch<{ status: string }>('/api/story/resume', { method: 'POST' }); -} - -export async function storySkip() { - return jsonFetch<{ previous_step: string; current_step: string }>( - '/api/story/skip', - { method: 'POST' }, - ); -} - -export async function storyValidate(yaml: string) { - return jsonFetch<{ valid: boolean }>('/api/story/validate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ yaml }), - }); -} - -export async function storyDeploy(yaml: string) { - return jsonFetch<{ deployed: string; status: string }>('/api/story/deploy', { - method: 'POST', - headers: { 'Content-Type': 'application/x-yaml' }, - body: yaml, - }); -} - -// ─── Legacy / Status ─── - -export interface LegacyStatus { - story: { - scenario: string; - step: string; - screen?: string; - audio_pack?: string; - runtime_contract?: string; - }; - runtime3?: Runtime3FirmwareStatus; - network?: { - state?: string; - ip?: string; - }; - media?: MediaStatus; -} - -export interface Runtime3FirmwareStatus { - discovered: boolean; - loaded: boolean; - path: string; - schema_version: string; - scenario_id: string; - scenario_version: number; - entry_step_id: string; - source_kind: string; - generated_by: string; - migration_mode: string; - step_count: number; - transition_count: number; - size_bytes: number; - error: string; -} - -export interface MediaStatus { - ready: boolean; - playing: boolean; - recording: boolean; - record_limit_seconds: number; - record_elapsed_seconds: number; - record_file: string; - record_simulated: boolean; - music_dir: string; - picture_dir: string; - record_dir: string; - last_ok: boolean; - last_error: string; -} - -export async function legacyStatus(): Promise { - return jsonFetch('/api/status'); -} - -export async function runtime3Status(): Promise { - return jsonFetch('/api/runtime3/status'); -} - -export async function runtime3Document(): Promise> { - return jsonFetch>('/api/runtime3/document'); -} - -// ─── Media ─── - -export interface MediaFileList { - ok: boolean; - kind: string; - files: string[]; -} - -export async function mediaFiles( - kind: 'music' | 'picture' | 'recorder', -): Promise { - return jsonFetch( - `/api/media/files?kind=${encodeURIComponent(kind)}`, - ); -} - -export async function mediaPlay(path: string) { - return jsonFetch<{ action: string; ok: boolean }>('/api/media/play', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path }), - }); -} - -export async function mediaStop() { - return jsonFetch<{ action: string; ok: boolean }>('/api/media/stop', { - method: 'POST', - }); -} - -export async function mediaRecordStart(seconds: number, filename: string) { - return jsonFetch<{ action: string; ok: boolean }>( - '/api/media/record/start', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ seconds, filename }), - }, - ); -} - -export async function mediaRecordStop() { - return jsonFetch<{ action: string; ok: boolean }>('/api/media/record/stop', { - method: 'POST', - }); -} - -// ─── Network ─── - -export async function networkReconnect() { - return jsonFetch<{ action: string; ok: boolean }>( - '/api/network/wifi/reconnect', - { method: 'POST' }, - ); -} - -export async function espnowOn() { - return jsonFetch<{ action: string; ok: boolean }>('/api/network/espnow/on', { - method: 'POST', - }); -} - -export async function espnowOff() { - return jsonFetch<{ action: string; ok: boolean }>('/api/network/espnow/off', { - method: 'POST', - }); -} - -// ─── Control (legacy fallback) ─── - -export async function legacyControl(action: string) { - return jsonFetch<{ ok: boolean; action: string }>('/api/control', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action }), - }); -} - -// ─── WebSocket Stream ─── - -export type StreamMessage = { - type: 'status' | 'step_change' | 'transition' | 'audit_log'; - timestamp: number; - data: Record; -}; - -export function connectStoryStream( - onMessage: (msg: StreamMessage) => void, - onError?: (err: Event) => void, -): { close: () => void } { - const MAX_BACKOFF_MS = 8000; - let attempt = 0; - let ws: WebSocket | null = null; - let closed = false; - let reconnectTimer: ReturnType | null = null; - - function connect() { - if (closed) return; - const wsUrl = baseUrl.replace(/^http/, 'ws') + '/api/story/stream'; - ws = new WebSocket(wsUrl); - - ws.onopen = () => { - attempt = 0; - }; - - ws.onmessage = (ev) => { - try { - onMessage(JSON.parse(ev.data)); - } catch { - /* ignore parse errors */ - } - }; - - ws.onerror = (err) => { - if (onError) onError(err); - }; - - ws.onclose = () => { - if (closed) return; - const delay = Math.min(1000 * Math.pow(2, attempt), MAX_BACKOFF_MS); - attempt++; - reconnectTimer = setTimeout(connect, delay); - }; - } - - connect(); - - return { - close() { - closed = true; - if (reconnectTimer !== null) clearTimeout(reconnectTimer); - ws?.close(); - }, - }; -} - -// ─── SSE Stream (legacy) ─── - -export function connectLegacyStream( - onStatus: (data: LegacyStatus) => void, -): EventSource { - const es = new EventSource(`${baseUrl}/api/stream`); - es.addEventListener('status', (ev) => { - try { - onStatus(JSON.parse((ev as MessageEvent).data)); - } catch { - /* ignore */ - } - }); - return es; -} - -// --------------------------------------------------------------------------- -// Voice pipeline -// --------------------------------------------------------------------------- - -export interface VoiceStatus { - connected: boolean; - state: number; - has_audio: boolean; - last_response?: string; -} - -export async function voiceStatus(): Promise { - return jsonFetch(`${baseUrl}/api/voice/status`); -} - -export async function voiceQuery(text: string): Promise<{ status: string }> { - return jsonFetch<{ status: string }>(`${baseUrl}/api/voice/query`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text }), - }); -} - -// ─── Analytics ─── - -export interface GameAnalytics { - session_id: string; - duration_ms: number; - puzzles_solved: number; - total_hints: number; - total_attempts: number; - puzzles: Array<{ - puzzle_id: string; - solved: boolean; - attempts: number; - hints: number; - duration_ms: number; - }>; -} - -export async function gameAnalytics(): Promise { - return jsonFetch(`${baseUrl}/api/analytics`); -} - -// ─── Hints (via mascarade) ─── - -export async function askHint(puzzleId: string, question: string, hintLevel: number): Promise<{ hint: string; hint_count: number }> { - return jsonFetch<{ hint: string; hint_count: number }>(`${baseUrl}/api/voice/query`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: `[HINT:${puzzleId}:${hintLevel}] ${question}` }), - }); -} diff --git a/frontend-scratch-v2/src/lib/runtime3.ts b/frontend-scratch-v2/src/lib/runtime3.ts deleted file mode 100644 index a61ed29..0000000 --- a/frontend-scratch-v2/src/lib/runtime3.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { ScenarioDocument, StepTransition } from '../types'; - -export type Runtime3EventType = - | 'button' - | 'serial' - | 'timer' - | 'audio_done' - | 'unlock' - | 'espnow' - | 'action'; - -export interface Runtime3Transition { - id: string; - event_type: Runtime3EventType; - event_name: string; - target_step_id: string; - priority: number; - after_ms: number; -} - -export interface Runtime3Step { - id: string; - scene_id: string; - audio_pack_id: string; - actions: string[]; - apps: string[]; - transitions: Runtime3Transition[]; -} - -export interface Runtime3Document { - schema_version: 'zacus.runtime3.v1'; - scenario: { - id: string; - version: number; - title: string; - entry_step_id: string; - source_kind: 'studio'; - }; - steps: Runtime3Step[]; - metadata: { - generated_by: 'frontend-scratch-v2'; - migration_mode: 'native'; - }; -} - -function normalizeToken(raw: string, fallback: string): string { - const cleaned = raw - .trim() - .replace(/[^a-zA-Z0-9_]+/g, '_') - .replace(/^_+|_+$/g, '') - .toUpperCase(); - return cleaned || fallback; -} - -function normalizeEventType(raw: string): Runtime3EventType { - const allowed: Runtime3EventType[] = [ - 'button', - 'serial', - 'timer', - 'audio_done', - 'unlock', - 'espnow', - 'action', - ]; - return allowed.includes(raw as Runtime3EventType) - ? (raw as Runtime3EventType) - : 'serial'; -} - -function normalizeTransition( - transition: Pick & { - eventType: string; - }, - index: number, -): Runtime3Transition { - return { - id: `TR_${index + 1}_${normalizeToken(transition.targetStepId, 'STEP_NEXT')}`, - event_type: normalizeEventType(transition.eventType), - event_name: normalizeToken(transition.eventName, 'BTN_NEXT'), - target_step_id: normalizeToken(transition.targetStepId, 'STEP_NEXT'), - priority: transition.priority ?? 0, - after_ms: transition.afterMs ?? 0, - }; -} - -export function compileScenarioDocumentToRuntime3( - document: ScenarioDocument, -): Runtime3Document { - const steps = (document.firmware?.steps ?? []).map((step, index) => ({ - id: normalizeToken(step.step_id, `STEP_${index + 1}`), - scene_id: normalizeToken(step.screen_scene_id, `SCENE_${index + 1}`), - audio_pack_id: normalizeToken(step.audio_pack_id, ''), - actions: step.actions ?? [], - apps: step.apps ?? [], - transitions: (step.transitions ?? []).map((transition, transitionIndex) => - normalizeTransition( - { - eventType: transition.event_type, - eventName: transition.event_name, - targetStepId: transition.target_step_id, - priority: transition.priority, - afterMs: transition.after_ms, - }, - transitionIndex, - ), - ), - })); - - const entryStepId = - normalizeToken( - document.firmware?.initial_step ?? - document.firmware?.steps?.[0]?.step_id ?? - document.firmware?.steps_reference_order?.[0] ?? - document.steps_narrative[0]?.step_id ?? - '', - 'STEP_BOOT', - ) || 'STEP_BOOT'; - - return { - schema_version: 'zacus.runtime3.v1', - scenario: { - id: normalizeToken(document.id, 'ZACUS_RUNTIME3'), - version: document.version, - title: document.title, - entry_step_id: entryStepId, - source_kind: 'studio', - }, - steps, - metadata: { - generated_by: 'frontend-scratch-v2', - migration_mode: 'native', - }, - }; -} - -export function validateRuntime3Document( - document: Runtime3Document, -): { ok: true } | { ok: false; error: string } { - const stepIds = new Set(); - - if (document.steps.length === 0) { - return { ok: false, error: 'runtime3 requires at least one step' }; - } - - for (const step of document.steps) { - if (stepIds.has(step.id)) { - return { ok: false, error: `duplicate runtime3 step id: ${step.id}` }; - } - stepIds.add(step.id); - } - - if (!stepIds.has(document.scenario.entry_step_id)) { - return { ok: false, error: 'entry_step_id does not exist in runtime3 steps' }; - } - - for (const step of document.steps) { - for (const transition of step.transitions) { - if (!stepIds.has(transition.target_step_id)) { - return { - ok: false, - error: `transition target missing: ${transition.target_step_id}`, - }; - } - } - } - - return { ok: true }; -} - -export function runtime3ToJson(document: Runtime3Document): string { - return JSON.stringify(document, null, 2); -} diff --git a/frontend-scratch-v2/src/lib/scenario.ts b/frontend-scratch-v2/src/lib/scenario.ts deleted file mode 100644 index 6146b9a..0000000 --- a/frontend-scratch-v2/src/lib/scenario.ts +++ /dev/null @@ -1,294 +0,0 @@ -import YAML from 'yaml'; -import { z } from 'zod'; -import type { ScenarioDocument, ScenarioStep, StepTransition } from '../types'; - -const scenarioSchema = z.object({ - id: z.string().min(1), - version: z.number().int().positive(), - title: z.string().min(1), - players: z.object({ - min: z.number().int().min(1), - max: z.number().int().min(1), - }), - duration: z.object({ - total_minutes: z.number().int().positive(), - }), - canon: z.object({ - introduction: z.string().min(1), - stakes: z.string().min(1), - }), - stations: z.array(z.record(z.string(), z.unknown())), - puzzles: z.array(z.record(z.string(), z.unknown())), - steps_narrative: z.array( - z.object({ - step_id: z.string().min(1), - scene: z.string().min(1), - narrative: z.string().min(1), - }), - ), - firmware: z - .object({ - initial_step: z.string().min(1), - steps_reference_order: z.array(z.string().min(1)).optional(), - steps: z.array( - z.object({ - step_id: z.string().min(1), - screen_scene_id: z.string().min(1), - audio_pack_id: z.string(), - actions: z.array(z.string()), - apps: z.array(z.string()), - transitions: z.array( - z.object({ - event_type: z.string().min(1), - event_name: z.string().min(1), - target_step_id: z.string().min(1), - priority: z.number().int(), - after_ms: z.number().int().min(0), - }), - ), - }), - ), - }) - .optional(), -}); - -export function normalizeId(raw: string, fallback: string): string { - const cleaned = raw - .trim() - .replace(/[^a-zA-Z0-9_]+/g, '_') - .replace(/^_+|_+$/g, '') - .toUpperCase(); - return cleaned || fallback; -} - -function normalizeTransition(transition: StepTransition): StepTransition { - return { - eventType: transition.eventType, - eventName: normalizeId(transition.eventName, 'BTN_NEXT'), - targetStepId: normalizeId(transition.targetStepId, 'STEP_NEXT'), - priority: transition.priority ?? 0, - afterMs: transition.afterMs ?? 0, - }; -} - -function resolveScenarioStepOrder(document: ScenarioDocument): string[] { - if (Array.isArray(document.firmware?.steps) && document.firmware.steps.length > 0) { - return document.firmware.steps.map((step) => step.step_id); - } - if ( - Array.isArray(document.firmware?.steps_reference_order) && - document.firmware.steps_reference_order.length > 0 - ) { - return document.firmware.steps_reference_order; - } - return document.steps_narrative.map((step) => step.step_id); -} - -export function buildScenarioFromBlocks( - scenarioId: string, - steps: ScenarioStep[], -): ScenarioDocument { - const normalizedSteps = steps.map((step, index) => { - const position = index + 1; - return { - stepId: normalizeId(step.stepId, `STEP_${position}`), - sceneId: normalizeId(step.sceneId, `SCENE_${position}`), - audioPack: step.audioPack ?? '', - actions: step.actions ?? [], - apps: step.apps ?? [], - transitions: (step.transitions ?? []).map(normalizeTransition), - }; - }); - - const fallbackStep = - normalizedSteps.length > 0 - ? normalizedSteps - : [ - { - stepId: 'STEP_BOOT', - sceneId: 'SCENE_BOOT', - audioPack: '', - actions: [], - apps: [], - transitions: [], - }, - ]; - - const firmwareSteps = fallbackStep.map((step, i) => ({ - step_id: step.stepId, - screen_scene_id: step.sceneId, - audio_pack_id: step.audioPack, - actions: step.actions, - apps: step.apps, - transitions: step.transitions.length - ? step.transitions.map((transition) => ({ - event_type: transition.eventType, - event_name: transition.eventName, - target_step_id: transition.targetStepId, - priority: transition.priority, - after_ms: transition.afterMs, - })) - : i < fallbackStep.length - 1 - ? [ - { - event_type: 'serial', - event_name: 'BTN_NEXT', - target_step_id: fallbackStep[i + 1].stepId, - priority: 0, - after_ms: 0, - }, - ] - : [], - })); - - return { - id: normalizeId(scenarioId, 'ZACUS_V2_NEW'), - version: 1, - title: 'Nouveau scenario Zacus', - players: { min: 6, max: 14 }, - duration: { total_minutes: 105 }, - canon: { - introduction: 'Scenario genere depuis frontend scratch-like.', - stakes: 'Valider les transitions et deployer via API Story V2.', - }, - stations: [], - puzzles: [], - steps_narrative: fallbackStep.map((s) => ({ - step_id: s.stepId, - scene: s.sceneId, - narrative: `Etape ${s.stepId} en scene ${s.sceneId}.`, - })), - firmware: { - initial_step: fallbackStep[0].stepId, - steps_reference_order: fallbackStep.map((step) => step.stepId), - steps: firmwareSteps, - }, - }; -} - -export function validateScenarioDocument( - document: ScenarioDocument, -): { ok: true } | { ok: false; error: string } { - const parsed = scenarioSchema.safeParse(document); - if (!parsed.success) { - return { - ok: false, - error: parsed.error.issues[0]?.message ?? 'schema validation error', - }; - } - - if (parsed.data.players.min > parsed.data.players.max) { - return { ok: false, error: 'players.min must be <= players.max' }; - } - - const stepOrder = resolveScenarioStepOrder(parsed.data); - const uniqueStepIds = new Set(stepOrder); - if (uniqueStepIds.size !== stepOrder.length) { - return { ok: false, error: 'duplicate step ids in scenario step order' }; - } - - if (parsed.data.firmware) { - const runtimeStepIds = new Set(parsed.data.firmware.steps.map((step) => step.step_id)); - if (!runtimeStepIds.has(parsed.data.firmware.initial_step)) { - return { ok: false, error: 'firmware.initial_step must exist in firmware.steps' }; - } - for (const step of parsed.data.firmware.steps) { - for (const transition of step.transitions) { - if (!runtimeStepIds.has(transition.target_step_id)) { - return { - ok: false, - error: `transition target missing: ${transition.target_step_id}`, - }; - } - } - } - if ( - Array.isArray(parsed.data.firmware.steps_reference_order) && - parsed.data.firmware.steps_reference_order.length > 0 && - parsed.data.firmware.steps_reference_order.join('|') !== - parsed.data.firmware.steps.map((step) => step.step_id).join('|') - ) { - return { - ok: false, - error: 'firmware.steps_reference_order must match firmware.steps order when both are present', - }; - } - } - - return { ok: true }; -} - -export function scenarioToYaml(document: ScenarioDocument): string { - return YAML.stringify(document); -} - -/** Parse a YAML string back into steps for the Blockly designer */ -export function parseYamlToSteps( - yamlStr: string, -): { id: string; steps: ScenarioStep[] } | { error: string } { - try { - const data = YAML.parse(yamlStr); - if (!data || typeof data !== 'object') { - return { error: 'Invalid YAML: not an object' }; - } - - const id = data.id ?? 'UNKNOWN'; - const steps: ScenarioStep[] = []; - - const normalizeTransitionList = (items: unknown): StepTransition[] => { - if (!Array.isArray(items)) return []; - return items.map((transition) => ({ - eventType: - transition?.event_type ?? - transition?.eventType ?? - transition?.trigger ?? - 'serial', - eventName: transition?.event_name ?? transition?.eventName ?? 'BTN_NEXT', - targetStepId: - transition?.target_step_id ?? transition?.targetStepId ?? transition?.target ?? '', - priority: Number(transition?.priority ?? 0), - afterMs: Number(transition?.after_ms ?? transition?.afterMs ?? 0), - })); - }; - - const fwSteps = data.runtime3?.steps ?? data.firmware?.steps ?? data.steps; - if (Array.isArray(fwSteps)) { - for (const s of fwSteps) { - steps.push({ - stepId: s.id ?? s.step_id ?? s.stepId ?? '', - sceneId: s.scene_id ?? s.screen_scene_id ?? s.sceneId ?? s.scene ?? '', - audioPack: s.audio_pack_id ?? '', - actions: s.actions ?? [], - apps: s.apps ?? [], - transitions: normalizeTransitionList(s.transitions), - }); - } - } - - // Fallback: steps_narrative - if (steps.length === 0 && Array.isArray(data.steps_narrative)) { - for (const s of data.steps_narrative) { - steps.push({ - stepId: s.step_id ?? '', - sceneId: s.scene ?? '', - transitions: [], - }); - } - } - - // Fallback: firmware.steps_reference_order - if (steps.length === 0 && Array.isArray(data.firmware?.steps_reference_order)) { - for (const id of data.firmware.steps_reference_order) { - steps.push({ stepId: id, sceneId: id, transitions: [] }); - } - } - - if (steps.length === 0) { - return { error: 'No steps found in YAML' }; - } - - return { id, steps }; - } catch (err) { - return { error: `YAML parse error: ${err instanceof Error ? err.message : err}` }; - } -} diff --git a/frontend-scratch-v2/src/lib/useRuntimeStore.ts b/frontend-scratch-v2/src/lib/useRuntimeStore.ts deleted file mode 100644 index eb08423..0000000 --- a/frontend-scratch-v2/src/lib/useRuntimeStore.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { - type LegacyStatus, - type StoryStatus, - type StreamMessage, - connectStoryStream, - legacyStatus, - storyStatus, -} from './api'; - -export interface RuntimeState { - connected: boolean; - story: StoryStatus | null; - legacy: LegacyStatus | null; - lastError: string; - lastEvent: StreamMessage | null; -} - -const POLL_INTERVAL = 3000; - -export function useRuntimeStore() { - const [state, setState] = useState({ - connected: false, - story: null, - legacy: null, - lastError: '', - lastEvent: null, - }); - const wsRef = useRef<{ close: () => void } | null>(null); - const timerRef = useRef | null>(null); - - const poll = useCallback(async () => { - try { - const [story, legacy] = await Promise.all([ - storyStatus().catch(() => null), - legacyStatus().catch(() => null), - ]); - setState((prev) => ({ - ...prev, - connected: !!(story || legacy), - story: story ?? prev.story, - legacy: legacy ?? prev.legacy, - lastError: '', - })); - } catch (err) { - setState((prev) => ({ - ...prev, - connected: false, - lastError: err instanceof Error ? err.message : String(err), - })); - } - }, []); - - useEffect(() => { - poll(); - timerRef.current = setInterval(poll, POLL_INTERVAL); - - try { - const ws = connectStoryStream( - (msg) => { - setState((prev) => ({ ...prev, lastEvent: msg })); - if (msg.type === 'step_change' || msg.type === 'status') { - poll(); - } - }, - () => { - /* ws error — polling will keep us alive */ - }, - ); - wsRef.current = ws; - } catch { - /* no ws support */ - } - - return () => { - if (timerRef.current) clearInterval(timerRef.current); - if (wsRef.current) wsRef.current.close(); - }; - }, [poll]); - - return { ...state, refresh: poll }; -} - -/** Detect if we're on the Media Manager screen */ -export function isMediaManagerActive(legacy: LegacyStatus | null): boolean { - if (!legacy) return false; - const screen = legacy.story.screen ?? ''; - const step = legacy.story.step ?? ''; - return ( - screen === 'SCENE_MEDIA_MANAGER' || - step === 'STEP_MEDIA_MANAGER' || - screen.includes('MEDIA_MANAGER') || - step.includes('MEDIA_MANAGER') - ); -} diff --git a/frontend-scratch-v2/src/main.tsx b/frontend-scratch-v2/src/main.tsx deleted file mode 100644 index bef5202..0000000 --- a/frontend-scratch-v2/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/frontend-scratch-v2/src/types.ts b/frontend-scratch-v2/src/types.ts deleted file mode 100644 index 187dcd8..0000000 --- a/frontend-scratch-v2/src/types.ts +++ /dev/null @@ -1,66 +0,0 @@ -export interface ScenarioStep { - stepId: string; - sceneId: string; - audioPack?: string; - actions?: string[]; - apps?: string[]; - transitions?: StepTransition[]; -} - -export interface StepTransition { - eventType: - | 'button' - | 'serial' - | 'timer' - | 'audio_done' - | 'unlock' - | 'espnow' - | 'action'; - eventName: string; - targetStepId: string; - priority: number; - afterMs: number; -} - -export interface ScenarioDocument { - id: string; - version: number; - title: string; - players: { - min: number; - max: number; - }; - duration: { - total_minutes: number; - }; - canon: { - introduction: string; - stakes: string; - }; - stations: Array>; - puzzles: Array>; - steps_narrative: Array<{ - step_id: string; - scene: string; - narrative: string; - }>; - firmware?: { - initial_step?: string; - steps_reference_order?: string[]; - steps?: Array<{ - step_id: string; - screen_scene_id: string; - audio_pack_id: string; - actions: string[]; - apps: string[]; - transitions: Array<{ - trigger?: string; - event_type: string; - event_name: string; - target_step_id: string; - priority: number; - after_ms: number; - }>; - }>; - }; -} diff --git a/frontend-scratch-v2/tests/scenario-runtime3.test.ts b/frontend-scratch-v2/tests/scenario-runtime3.test.ts deleted file mode 100644 index 00732b4..0000000 --- a/frontend-scratch-v2/tests/scenario-runtime3.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import YAML from 'yaml'; -import { buildScenarioFromBlocks, parseYamlToSteps, validateScenarioDocument } from '../src/lib/scenario'; -import { compileScenarioDocumentToRuntime3, validateRuntime3Document } from '../src/lib/runtime3'; -import type { ScenarioDocument } from '../src/types'; - -function buildGraphFirstDocument(): ScenarioDocument { - return { - id: 'zacus_graph_first', - version: 3, - title: 'Zacus Graph First', - players: { min: 6, max: 12 }, - duration: { total_minutes: 90 }, - canon: { - introduction: 'Intro', - stakes: 'Stakes', - }, - stations: [], - puzzles: [], - steps_narrative: [ - { step_id: 'STEP_BOOT', scene: 'SCENE_BOOT', narrative: 'Boot' }, - { step_id: 'STEP_GATE', scene: 'SCENE_GATE', narrative: 'Gate' }, - { step_id: 'STEP_DONE', scene: 'SCENE_DONE', narrative: 'Done' }, - ], - firmware: { - initial_step: 'STEP_BOOT', - steps: [ - { - step_id: 'STEP_BOOT', - screen_scene_id: 'SCENE_BOOT', - audio_pack_id: '', - actions: [], - apps: [], - transitions: [ - { - event_type: 'serial', - event_name: 'BTN_NEXT', - target_step_id: 'STEP_GATE', - priority: 100, - after_ms: 0, - }, - ], - }, - { - step_id: 'STEP_GATE', - screen_scene_id: 'SCENE_GATE', - audio_pack_id: '', - actions: [], - apps: [], - transitions: [ - { - event_type: 'unlock', - event_name: 'UNLOCK_GATE', - target_step_id: 'STEP_DONE', - priority: 120, - after_ms: 0, - }, - ], - }, - { - step_id: 'STEP_DONE', - screen_scene_id: 'SCENE_DONE', - audio_pack_id: '', - actions: [], - apps: [], - transitions: [], - }, - ], - }, - }; -} - -describe('graph-first scenario handling', () => { - it('validates a canonical graph-first document without steps_reference_order', () => { - const document = buildGraphFirstDocument(); - expect(validateScenarioDocument(document)).toEqual({ ok: true }); - }); - - it('parses firmware.steps from YAML before older fallback fields', () => { - const yaml = YAML.stringify(buildGraphFirstDocument()); - const parsed = parseYamlToSteps(yaml); - if ('error' in parsed) { - throw new Error(parsed.error); - } - expect(parsed.steps.map((step) => step.stepId)).toEqual([ - 'STEP_BOOT', - 'STEP_GATE', - 'STEP_DONE', - ]); - expect(parsed.steps[1]?.transitions?.[0]?.eventType).toBe('unlock'); - }); - - it('compiles runtime3 entry step from firmware.initial_step', () => { - const runtime3 = compileScenarioDocumentToRuntime3(buildGraphFirstDocument()); - expect(runtime3.scenario.entry_step_id).toBe('STEP_BOOT'); - expect(runtime3.steps[1]?.transitions[0]?.event_type).toBe('unlock'); - expect(validateRuntime3Document(runtime3)).toEqual({ ok: true }); - }); - - it('keeps the builder output compatible with the graph-first validator', () => { - const built = buildScenarioFromBlocks('zacus_builder', [ - { - stepId: 'STEP_BOOT', - sceneId: 'SCENE_BOOT', - transitions: [ - { - eventType: 'serial', - eventName: 'BTN_NEXT', - targetStepId: 'STEP_DONE', - priority: 50, - afterMs: 0, - }, - ], - }, - { - stepId: 'STEP_DONE', - sceneId: 'SCENE_DONE', - transitions: [], - }, - ]); - expect(validateScenarioDocument(built)).toEqual({ ok: true }); - expect(built.firmware?.steps?.[0]?.step_id).toBe('STEP_BOOT'); - expect(built.firmware?.steps_reference_order).toEqual(['STEP_BOOT', 'STEP_DONE']); - expect('steps_reference_order' in built).toBe(false); - }); -}); diff --git a/frontend-scratch-v2/tsconfig.app.json b/frontend-scratch-v2/tsconfig.app.json deleted file mode 100644 index a9b5a59..0000000 --- a/frontend-scratch-v2/tsconfig.app.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "types": ["vite/client"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] -} diff --git a/frontend-scratch-v2/tsconfig.json b/frontend-scratch-v2/tsconfig.json deleted file mode 100644 index 1ffef60..0000000 --- a/frontend-scratch-v2/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/frontend-scratch-v2/tsconfig.node.json b/frontend-scratch-v2/tsconfig.node.json deleted file mode 100644 index 8a67f62..0000000 --- a/frontend-scratch-v2/tsconfig.node.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/frontend-scratch-v2/vite.config.ts b/frontend-scratch-v2/vite.config.ts deleted file mode 100644 index 8b0f57b..0000000 --- a/frontend-scratch-v2/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}) diff --git a/frontend-v3/CLAUDE.md b/frontend-v3/CLAUDE.md index 33c43d7..bc13939 100644 --- a/frontend-v3/CLAUDE.md +++ b/frontend-v3/CLAUDE.md @@ -7,7 +7,7 @@ pnpm + turbo monorepo. Three apps + three shared packages. Node ≥20, pnpm ≥9 ``` apps/ dashboard/ # Live game-master dashboard (analytics, control) - editor/ # Scenario editor (V3 successor of frontend-scratch-v2) + editor/ # Scenario editor (will fuse with simulation into atelier — see docs/superpowers/specs/2026-05-01-v3-fusion-atelier-design.md) simulation/ # Runtime simulator / playtest UI packages/ scenario-engine/ # Runtime 3 IR + execution (shared core) @@ -34,12 +34,6 @@ pnpm lint # turbo run lint - Components shared across ≥2 apps move to `packages/ui`. Keep app-local components in `apps//src/components`. - Turbo cache keys: ensure `inputs` in `turbo.json` cover all source paths; missing globs cause stale cache. -## V2 vs V3 - -- `frontend-scratch-v2/` is the legacy single-app Vite playground. V3 is the production target. -- When porting features from V2, extract reusable logic into `packages/scenario-engine` or `packages/shared` first. -- Do not symlink or import across V2/V3 boundaries. - ## Anti-Patterns - Adding deps to root `package.json` (use the relevant app/package) diff --git a/specs/CLAUDE.md b/specs/CLAUDE.md index b7cefe1..394afd2 100644 --- a/specs/CLAUDE.md +++ b/specs/CLAUDE.md @@ -21,7 +21,7 @@ Contracts between firmware, frontends, tooling, and AI agents. Treat each spec a - A spec change is a contract change. List affected consumers in the PR description. - Bump the version header at the top of the file when altering existing fields; never silently rename keys. - New spec docs must be linked from root `CLAUDE.md` "Canonical Files" if they introduce a new contract. -- Pair runtime/frontend spec changes — the matching `tools/scenario/compile_runtime3.py` and `frontend-scratch-v2/src/lib/runtime3.ts` updates ship in the same PR. +- Pair runtime/frontend spec changes — the matching `tools/scenario/compile_runtime3.py` and `frontend-v3/packages/scenario-engine/src/engine.ts` updates ship in the same PR. ## Anti-Patterns diff --git a/tools/dev/zacus.sh b/tools/dev/zacus.sh index be34c74..c9c419d 100755 --- a/tools/dev/zacus.sh +++ b/tools/dev/zacus.sh @@ -17,7 +17,7 @@ PORTS_ARTIFACT_ROOT="$ARTIFACT_ROOT/ports" LATEST_PORTS_JSON="$PORTS_ARTIFACT_ROOT/latest_ports_resolve.json" CODEX_ARTIFACT_ROOT="$ARTIFACT_ROOT/codex" RUNTIME3_ARTIFACT_ROOT="$ARTIFACT_ROOT/runtime3" -FRONTEND_ROOT="$REPO_ROOT/frontend-scratch-v2" +FRONTEND_ROOT="$REPO_ROOT/frontend-v3" DEFAULT_SCENARIO="$REPO_ROOT/game/scenarios/zacus_v2.yaml" DEFAULT_PORTS_FIXTURE="$REPO_ROOT/tools/test/fixtures/ports_list_macos.txt" LAST_RESOLVED_JSON="" @@ -46,9 +46,9 @@ Commands: runtime3-verify Verify Runtime 3 pivot coverage on the canonical scenario runtime3-test Run Runtime 3 unit tests runtime3-firmware-bundle Export Runtime 3 JSON into the firmware LittleFS tree - frontend-lint Run the React/Blockly studio lint gate - frontend-test Run the React/Blockly studio test gate - frontend-build Run the React/Blockly studio build gate + frontend-typecheck Run the V3 monorepo typecheck gate + frontend-test Run the V3 monorepo test gate + frontend-build Run the V3 monorepo build gate docs-build Build the MkDocs site in strict mode artifacts-summary Show recent Zacus artifact directories artifacts-prune Delete old Zacus artifact directories (explicit --yes required) @@ -353,19 +353,19 @@ HELP (cd "$REPO_ROOT" && python3 tools/scenario/export_runtime3_firmware_bundle.py "$scenario" -o "$out_file") } -cmd_frontend_lint() { - info "running frontend lint" - (cd "$FRONTEND_ROOT" && npm run lint) +cmd_frontend_typecheck() { + info "running frontend typecheck" + (cd "$FRONTEND_ROOT" && pnpm typecheck) } cmd_frontend_test() { info "running frontend tests" - (cd "$FRONTEND_ROOT" && npm run test) + (cd "$FRONTEND_ROOT" && pnpm test) } cmd_frontend_build() { info "running frontend build" - (cd "$FRONTEND_ROOT" && npm run build) + (cd "$FRONTEND_ROOT" && pnpm build) } cmd_docs_build() { @@ -802,8 +802,8 @@ case "$command" in runtime3-firmware-bundle) cmd_runtime3_firmware_bundle "$@" ;; - frontend-lint) - cmd_frontend_lint "$@" + frontend-typecheck) + cmd_frontend_typecheck "$@" ;; frontend-test) cmd_frontend_test "$@" diff --git a/tools/dev/zacus_tui.py b/tools/dev/zacus_tui.py index f2d68d3..249b935 100755 --- a/tools/dev/zacus_tui.py +++ b/tools/dev/zacus_tui.py @@ -204,16 +204,16 @@ def action_python_tests(): def action_frontend_tests(): return run_command( "Run Frontend Tests", - "npx vitest run", - cwd=str(REPO_ROOT / "frontend-scratch-v2"), + "pnpm test", + cwd=str(REPO_ROOT / "frontend-v3"), ) def action_build_frontend(): return run_command( "Build Frontend", - "npm run build", - cwd=str(REPO_ROOT / "frontend-scratch-v2"), + "pnpm build", + cwd=str(REPO_ROOT / "frontend-v3"), )