diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..bdcbc46
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,9 @@
+# Bake the in-browser simulator into the bundle and skip the server probe.
+# GitHub Pages sets this. Leave it unset locally so `npm run dev` uses the Node simulator.
+# VITE_FORCE_BROWSER=true
+
+# Optional absolute WebSocket URL. Defaults to the page host + /ws (Vite proxies /ws in dev).
+# VITE_WS_URL=ws://127.0.0.1:8787/ws
+
+# Port for the Node simulator.
+# PORT=8787
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8fe22b7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - name: Install
+ run: npm ci
+ - name: Lint
+ run: npm run lint
+ - name: Typecheck
+ run: npm run typecheck
+ - name: Unit tests
+ run: npm test
+ - name: Install Playwright
+ run: npx playwright install --with-deps chromium
+ - name: Smoke test
+ run: npm run test:e2e
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..38c9f28
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,45 @@
+name: Deploy GitHub Pages
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - uses: actions/configure-pages@v5
+ - run: npm ci
+ - name: Build static demo
+ run: npm run build
+ env:
+ VITE_FORCE_BROWSER: 'true'
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: dist
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c7c95c8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+node_modules
+dist
+coverage
+playwright-report
+test-results
+blob-report
+.DS_Store
+*.local
+.env
+.env.*
+!.env.example
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..90742fc
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,6 @@
+dist
+node_modules
+coverage
+playwright-report
+test-results
+package-lock.json
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..a067b6a
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "printWidth": 96,
+ "singleQuote": true,
+ "semi": true,
+ "trailingComma": "all"
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ce06fe5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Arpan Bom
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 3e9ce7e..4250139 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,106 @@
-# react-ts-fleet-dashboard
-React + TypeScript real-time fleet monitoring dashboard
+# Meridian
+
+Real-time fleet operations console for ground vehicles and drones. This is a **portfolio demo** by [Arpan Bom](https://github.com/applycoding). The tracks, batteries, and alerts are simulated. Nothing here is connected to a live vehicle, a drone remote ID feed, or an operations center.
+
+[](https://github.com/applycoding/react-ts-fleet-dashboard/actions/workflows/ci.yml)
+
+The same deterministic simulator runs in two places:
+
+- a small Node server with HTTP snapshots and a WebSocket tick, for local development
+- the browser itself, when that server is not there (including the GitHub Pages build)
+
+## Screenshots
+
+
+
+
+
+
+
+## Features
+
+- Live map (Leaflet, CARTO basemaps from OpenStreetMap data) with moving ground and drone markers and a restricted-airspace box
+- Unit list with search, kind, and status filters
+- Telemetry for the selected unit: speed, battery, altitude, and heading over the last few minutes
+- Alerts for low battery, intermittent links, and drones inside the restricted box, with acknowledge
+- Light, dark, and system color schemes, without a flash of the wrong scheme
+- Layout that stacks on narrow viewports
+- Vitest and Testing Library coverage, plus a Playwright smoke test
+- GitHub Actions CI and a GitHub Pages workflow that ships the in-browser simulator
+
+## Architecture
+
+```mermaid
+flowchart LR
+ subgraph browser [Browser]
+ UI[React panels]
+ Z[Zustand UI state]
+ Q[TanStack Query]
+ SIM[In-browser engine]
+ UI --> Z
+ UI --> Q
+ Q -->|server missing| SIM
+ end
+
+ subgraph node [Optional Node simulator]
+ HTTP[HTTP /api]
+ WS[WebSocket /ws]
+ ENG[Shared engine]
+ HTTP --> ENG
+ WS --> ENG
+ end
+
+ Q -->|local dev| HTTP
+ Q -->|local dev| WS
+```
+
+`src/sim` is the shared engine. Position is a function of time along a fixed route, so the server and the browser draw the same tracks. The React app keeps fleet snapshots and history in TanStack Query. Filters, the selected unit, acknowledgements, and the data-source mode live in Zustand.
+
+On startup the page asks `/api/health`. A response means “live socket”. A miss, or `VITE_FORCE_BROWSER=true` in the Pages build, means the tab runs the engine itself.
+
+## Stack
+
+React 18, TypeScript, Vite, TanStack Query, Zustand, Leaflet, Vitest, Testing Library, Playwright, ESLint, Prettier.
+
+## Run it
+
+```bash
+npm install
+npm run dev
+```
+
+- App: http://localhost:5173
+- Simulator: http://127.0.0.1:8787 (`/api/health`, `/api/snapshot`, `/api/units/:id/history`, `POST /api/alerts/:id/ack`, WebSocket `/ws`)
+
+The Vite dev server proxies `/api` and `/ws` to the simulator. Stop the simulator and reload: the badge switches to **Browser sim** and the map keeps moving.
+
+```bash
+npm run dev:server # simulator only
+npm run dev:client # Vite only
+npm run build
+npm run preview
+```
+
+`npm run build` writes a static `dist/` that does not need the Node process.
+
+## Tests
+
+```bash
+npm run lint
+npm test
+npm run test:e2e
+```
+
+`test:e2e` builds the static app, serves it, and checks that the in-browser fleet renders and the drone filter works. CI runs lint, the typecheck, unit tests, and that smoke test.
+
+## GitHub Pages
+
+`.github/workflows/pages.yml` builds with `VITE_FORCE_BROWSER=true` and deploys `dist/` with GitHub Pages actions. In the repository settings, set **Pages → Build and deployment → Source** to **GitHub Actions**. After the first deploy on `main` the site is at:
+
+https://applycoding.github.io/react-ts-fleet-dashboard/
+
+Map tiles are loaded from CARTO’s public basemap host. That is fine for a demo; it is not a tile service you should put in front of real traffic.
+
+## License
+
+[MIT](LICENSE) © 2026 Arpan Bom
diff --git a/docs/screenshots/desktop-dark.png b/docs/screenshots/desktop-dark.png
new file mode 100644
index 0000000..8a94901
Binary files /dev/null and b/docs/screenshots/desktop-dark.png differ
diff --git a/docs/screenshots/desktop-light.png b/docs/screenshots/desktop-light.png
new file mode 100644
index 0000000..c5f18fa
Binary files /dev/null and b/docs/screenshots/desktop-light.png differ
diff --git a/docs/screenshots/mobile-dark.png b/docs/screenshots/mobile-dark.png
new file mode 100644
index 0000000..13644bf
Binary files /dev/null and b/docs/screenshots/mobile-dark.png differ
diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts
new file mode 100644
index 0000000..2b926fc
--- /dev/null
+++ b/e2e/smoke.spec.ts
@@ -0,0 +1,19 @@
+import { expect, test } from '@playwright/test';
+
+test('shows the simulated fleet and filters to drones', async ({ page }) => {
+ await page.goto('./');
+ await expect(page.getByRole('heading', { level: 1, name: 'Meridian' })).toBeVisible();
+ await expect(page.getByRole('option', { name: /M-14/ })).toBeVisible();
+ await expect(page.getByText('Browser sim')).toBeVisible();
+
+ await page
+ .getByRole('group', { name: 'Unit kind' })
+ .getByRole('button', { name: 'Drones' })
+ .click();
+ await expect(page.getByRole('option', { name: /M-14/ })).toHaveCount(0);
+ await expect(page.getByRole('option', { name: /D-12/ })).toBeVisible();
+
+ await page.getByRole('option', { name: /D-12/ }).click();
+ await expect(page.getByRole('region', { name: 'Telemetry' })).toContainText('D-12');
+ await expect(page.getByRole('img', { name: /Speed in km\/h/ })).toBeVisible();
+});
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..c98b276
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,35 @@
+import js from '@eslint/js';
+import prettier from 'eslint-config-prettier';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ {
+ ignores: [
+ 'dist/**',
+ 'node_modules/**',
+ 'playwright-report/**',
+ 'test-results/**',
+ 'coverage/**',
+ ],
+ },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ files: ['**/*.{ts,tsx}'],
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
+ ],
+ },
+ },
+ prettier,
+);
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f78e504
--- /dev/null
+++ b/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ Meridian — Fleet operations
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..43c48f8
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,4799 @@
+{
+ "name": "meridian-fleet",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "meridian-fleet",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/react-query": "^5.104.2",
+ "leaflet": "^1.9.4",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-leaflet": "^4.2.1",
+ "zustand": "^5.0.15"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@playwright/test": "^1.64.1",
+ "@testing-library/jest-dom": "^7.0.1",
+ "@testing-library/react": "^16.3.3",
+ "@testing-library/user-event": "^14.6.8",
+ "@types/leaflet": "^1.9.22",
+ "@types/node": "^26.6.5",
+ "@types/react": "^18.3.31",
+ "@types/react-dom": "^18.3.7",
+ "@types/ws": "^8.18.2",
+ "@vitejs/plugin-react": "^6.1.2",
+ "concurrently": "^10.0.6",
+ "eslint": "^10.12.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.7",
+ "jsdom": "^29.1.1",
+ "prettier": "^3.9.10",
+ "tsx": "^4.23.15",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.71.2",
+ "vite": "^8.3.4",
+ "vitest": "^5.0.4",
+ "ws": "^8.22.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.10",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz",
+ "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
+ "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.10",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
+ "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.10",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz",
+ "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.9",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz",
+ "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@cacheable/memory": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
+ "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/utils": "^2.5.0",
+ "@keyv/bigmap": "^1.3.1",
+ "hookified": "^1.15.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@cacheable/utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
+ "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.5.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz",
+ "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz",
+ "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.2",
+ "@csstools/css-tokenizer": "^4.0.2"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz",
+ "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.1.2",
+ "@csstools/css-calc": "^3.4.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.2",
+ "@csstools/css-tokenizer": "^4.0.2"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz",
+ "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.2"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz",
+ "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz",
+ "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "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.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
+ "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz",
+ "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz",
+ "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.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.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "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/@keyv/bigmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
+ "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.4.0",
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@keyv/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.154.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
+ "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.64.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
+ "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.64.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@react-leaflet/core": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
+ "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz",
+ "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz",
+ "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz",
+ "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz",
+ "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz",
+ "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz",
+ "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz",
+ "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz",
+ "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz",
+ "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz",
+ "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz",
+ "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz",
+ "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz",
+ "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz",
+ "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz",
+ "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.104.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.2.tgz",
+ "integrity": "sha512-/qbsxJK5c3Ve1aFIULWYNcT2jeRUAiD4HME6Eu2ZdpofHoVgAjpkQhLg5xHuOoBkTPQUJLfP7zOxy0qsBcqROA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.104.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.2.tgz",
+ "integrity": "sha512-xKmzWGhadbE5wGS/ZmXShRITnI9oItox5GO14KvFZco3+vhTAyXS1erYzJWdiY280zapAGJXFUL9nHgwP6SV/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.104.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
+ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz",
+ "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=22",
+ "npm": ">=6",
+ "yarn": ">=1"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=10 <11",
+ "vitest": ">= 0.32"
+ },
+ "peerDependenciesMeta": {
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.8",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz",
+ "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "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/leaflet": {
+ "version": "1.9.22",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.22.tgz",
+ "integrity": "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "26.6.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
+ "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.9.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.2",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
+ "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz",
+ "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.71.2",
+ "@typescript-eslint/type-utils": "8.71.2",
+ "@typescript-eslint/utils": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.71.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz",
+ "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz",
+ "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.71.2",
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz",
+ "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.71.2",
+ "@typescript-eslint/types": "^8.71.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz",
+ "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2"
+ },
+ "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.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz",
+ "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz",
+ "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2",
+ "@typescript-eslint/utils": "8.71.2",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz",
+ "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==",
+ "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.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz",
+ "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.71.2",
+ "@typescript-eslint/tsconfig-utils": "8.71.2",
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz",
+ "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.71.2",
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz",
+ "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.71.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz",
+ "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxc-transform-react": "^0.152.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "oxc-transform-react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.4.tgz",
+ "integrity": "sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.31",
+ "@vitest/spy": "5.0.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^1.4.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9 || ^3.0.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.4.tgz",
+ "integrity": "sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
+ "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==",
+ "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/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.28",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz",
+ "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz",
+ "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.29.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz",
+ "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==",
+ "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.11.26",
+ "caniuse-lite": "^1.0.30001813",
+ "electron-to-chromium": "^1.5.439",
+ "node-releases": "^2.0.57",
+ "update-browserslist-db": "^1.3.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/cacheable": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
+ "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/memory": "^2.2.0",
+ "@cacheable/utils": "^2.5.0",
+ "hookified": "^1.15.0",
+ "keyv": "^5.6.0",
+ "qified": "^0.10.1"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001816",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz",
+ "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
+ "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/concurrently": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.6.tgz",
+ "integrity": "sha512-qgICXXFp2/3nAebRLmxcINKEXM1mLK3ij/y31P02VTs/KIkddIeC05n73DxOXsXMB9LFSqNU1/na3lDlOhcbIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "rxjs": "7.8.2",
+ "shell-quote": "1.12.0",
+ "supports-color": "10.2.2",
+ "tree-kill": "1.2.2",
+ "yargs": "18.0.0"
+ },
+ "bin": {
+ "conc": "dist/bin/index.js",
+ "concurrently": "dist/bin/index.js"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.456",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.456.tgz",
+ "integrity": "sha512-8hboqI6xesozteFb8aa7OGknvxUtTeRJLz0FoePysUEh/oWbddykIGdgpEJRbPRg/nIMfBf2DFos+H2MtvJl2A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.12.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz",
+ "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.7.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.3",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "11.1.5 || >11.1.6 <12",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz",
+ "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "11.1.5",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
+ "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^6.1.23"
+ }
+ },
+ "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": "6.1.23",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
+ "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cacheable": "^2.5.0",
+ "flatted": "^3.4.2",
+ "hookified": "^1.15.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "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/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz",
+ "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/hashery": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
+ "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "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/hookified": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
+ "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.5.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
+ "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "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-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "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/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
+ "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+ "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.58",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz",
+ "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/obug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
+ "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "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/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.64.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
+ "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.64.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.64.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
+ "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.30",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
+ "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==",
+ "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.20",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.2"
+ },
+ "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/prettier": {
+ "version": "3.9.10",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
+ "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qified": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
+ "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/qified/node_modules/hookified": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
+ "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-leaflet": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
+ "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^2.1.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
+ "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.154.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.14",
+ "@rolldown/binding-android-arm64": "1.2.14",
+ "@rolldown/binding-darwin-arm64": "1.2.14",
+ "@rolldown/binding-darwin-x64": "1.2.14",
+ "@rolldown/binding-freebsd-x64": "1.2.14",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.14",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.14",
+ "@rolldown/binding-linux-arm64-musl": "1.2.14",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.14",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.14",
+ "@rolldown/binding-linux-x64-gnu": "1.2.14",
+ "@rolldown/binding-linux-x64-musl": "1.2.14",
+ "@rolldown/binding-openharmony-arm64": "1.2.14",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.14",
+ "@rolldown/binding-win32-x64-msvc": "1.2.14"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz",
+ "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
+ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi/node_modules/ansi-regex": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz",
+ "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
+ "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.4.18",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.18.tgz",
+ "integrity": "sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.18"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.18",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.18.tgz",
+ "integrity": "sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/tsx": {
+ "version": "4.23.15",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
+ "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "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": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz",
+ "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.71.2",
+ "@typescript-eslint/parser": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2",
+ "@typescript-eslint/utils": "8.71.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.30.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
+ "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz",
+ "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==",
+ "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": "8.3.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
+ "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.7",
+ "postcss": "^8.5.29",
+ "rolldown": "~1.2.12",
+ "tinyglobby": "^0.2.17"
+ },
+ "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",
+ "@vitejs/devtools": "^0.7.1",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.4.tgz",
+ "integrity": "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/mocker": "5.0.4",
+ "chai": "^6.3.0",
+ "es-module-lexer": "^2.3.2",
+ "expect-type": "^1.4.0",
+ "magic-string": "^1.4.3",
+ "obug": "^3.0.0",
+ "picomatch": "^4.0.7",
+ "std-env": "^4.3.0",
+ "tinybench": "^6.2.2",
+ "tinyexec": "^1.3.1",
+ "tinyglobby": "^0.2.17",
+ "why-is-node-running": "3.2.1"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "5.0.4",
+ "@vitest/browser-preview": "5.0.4",
+ "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+ "@vitest/coverage-istanbul": "5.0.4",
+ "@vitest/coverage-v8": "5.0.4",
+ "@vitest/ui": "5.0.4",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz",
+ "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=20.11"
+ }
+ },
+ "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/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
+ "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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/yargs": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^7.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "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.6.7",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
+ "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
+ "dev": true,
+ "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"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz",
+ "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..88a05a6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "meridian-fleet",
+ "private": true,
+ "version": "1.0.0",
+ "description": "Portfolio demo: real-time vehicle and drone fleet monitor with a simulated telemetry feed.",
+ "author": "Arpan Bom (https://github.com/applycoding)",
+ "license": "MIT",
+ "type": "module",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/applycoding/react-ts-fleet-dashboard"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "scripts": {
+ "dev": "concurrently -n server,client -c cyan,magenta \"npm run dev:server\" \"npm run dev:client\"",
+ "dev:client": "vite",
+ "dev:server": "tsx watch server/index.ts",
+ "build": "npm run typecheck && vite build",
+ "preview": "vite preview",
+ "typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit",
+ "test": "vitest run",
+ "test:e2e": "playwright test",
+ "pretest:e2e": "npm run build",
+ "lint": "eslint . && prettier --check .",
+ "format": "prettier --write .",
+ "capture": "tsx scripts/capture-screenshots.ts"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.104.2",
+ "leaflet": "^1.9.4",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-leaflet": "^4.2.1",
+ "zustand": "^5.0.15"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@playwright/test": "^1.64.1",
+ "@testing-library/jest-dom": "^7.0.1",
+ "@testing-library/react": "^16.3.3",
+ "@testing-library/user-event": "^14.6.8",
+ "@types/leaflet": "^1.9.22",
+ "@types/node": "^26.6.5",
+ "@types/react": "^18.3.31",
+ "@types/react-dom": "^18.3.7",
+ "@types/ws": "^8.18.2",
+ "@vitejs/plugin-react": "^6.1.2",
+ "concurrently": "^10.0.6",
+ "eslint": "^10.12.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.7",
+ "jsdom": "^29.1.1",
+ "prettier": "^3.9.10",
+ "tsx": "^4.23.15",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.71.2",
+ "vite": "^8.3.4",
+ "vitest": "^5.0.4",
+ "ws": "^8.22.0"
+ }
+}
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..134c8d6
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: 'e2e',
+ timeout: 60_000,
+ use: {
+ baseURL: 'http://127.0.0.1:4173',
+ trace: 'on-first-retry',
+ },
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
+ webServer: {
+ command: 'npm run preview -- --host 127.0.0.1 --port 4173',
+ url: 'http://127.0.0.1:4173',
+ reuseExistingServer: !process.env.CI,
+ timeout: 30_000,
+ },
+});
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..dc70004
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,5 @@
+
diff --git a/scripts/capture-screenshots.ts b/scripts/capture-screenshots.ts
new file mode 100644
index 0000000..0b19245
--- /dev/null
+++ b/scripts/capture-screenshots.ts
@@ -0,0 +1,56 @@
+import { mkdir } from 'node:fs/promises';
+import { chromium, type Page } from '@playwright/test';
+
+const base = process.env.CAPTURE_URL ?? 'http://127.0.0.1:4173';
+
+async function waitForMap(page: Page): Promise {
+ await page
+ .getByRole('option', { name: /M-14|D-12/ })
+ .first()
+ .waitFor();
+ await page.evaluate('document.fonts.ready');
+ try {
+ await page.waitForFunction(
+ `() => [...document.querySelectorAll('img.leaflet-tile')].some((tile) => tile.complete && tile.naturalWidth > 0)`,
+ undefined,
+ { timeout: 12_000 },
+ );
+ } catch {
+ // A screenshot without tiles is still useful if the tile host is slow.
+ }
+ await page.waitForTimeout(400);
+}
+
+async function prepare(page: Page, scheme: 'light' | 'dark'): Promise {
+ await page.goto(base);
+ await page
+ .getByRole('button', { name: scheme === 'dark' ? 'Dark' : 'Light', exact: true })
+ .click();
+ await page.getByRole('option', { name: /D-12/ }).click();
+ await waitForMap(page);
+}
+
+async function main(): Promise {
+ await mkdir('docs/screenshots', { recursive: true });
+ const browser = await chromium.launch();
+
+ const desktop = await browser.newPage({ viewport: { width: 1440, height: 900 } });
+ await prepare(desktop, 'dark');
+ await desktop.screenshot({ path: 'docs/screenshots/desktop-dark.png' });
+
+ await prepare(desktop, 'light');
+ await desktop.screenshot({ path: 'docs/screenshots/desktop-light.png' });
+ await desktop.close();
+
+ const mobile = await browser.newPage({ viewport: { width: 390, height: 844 } });
+ await prepare(mobile, 'dark');
+ await mobile.screenshot({ path: 'docs/screenshots/mobile-dark.png', fullPage: true });
+ await mobile.close();
+
+ await browser.close();
+}
+
+main().catch((error: unknown) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/server/index.ts b/server/index.ts
new file mode 100644
index 0000000..24559e9
--- /dev/null
+++ b/server/index.ts
@@ -0,0 +1,15 @@
+import { createSimulatorServer } from './simulator';
+
+const parsed = Number(process.env.PORT ?? 8787);
+const port = Number.isFinite(parsed) ? parsed : 8787;
+const { server, close } = createSimulatorServer();
+
+server.listen(port, () => {
+ console.log(`Meridian simulator on http://127.0.0.1:${port}`);
+});
+
+for (const signal of ['SIGINT', 'SIGTERM'] as const) {
+ process.on(signal, () => {
+ void close().then(() => process.exit(0));
+ });
+}
diff --git a/server/server.test.ts b/server/server.test.ts
new file mode 100644
index 0000000..3bd8d13
--- /dev/null
+++ b/server/server.test.ts
@@ -0,0 +1,82 @@
+// @vitest-environment node
+import { afterEach, describe, expect, it } from 'vitest';
+import { WebSocket } from 'ws';
+import { createSimulatorServer } from './simulator';
+
+describe('simulator server', () => {
+ let closeServer: (() => Promise) | undefined;
+
+ afterEach(async () => {
+ await closeServer?.();
+ closeServer = undefined;
+ });
+
+ async function start(): Promise {
+ const simulator = createSimulatorServer();
+ closeServer = simulator.close;
+ await new Promise((resolve) => {
+ simulator.server.listen(0, '127.0.0.1', () => resolve());
+ });
+ const address = simulator.server.address();
+ if (!address || typeof address === 'string')
+ throw new Error('Simulator did not bind a port');
+ return address.port;
+ }
+
+ it('serves health, history, and acknowledgement', async () => {
+ const port = await start();
+ const health = await fetch(`http://127.0.0.1:${port}/api/health`);
+ expect(health.ok).toBe(true);
+
+ const snapshot = (await fetch(`http://127.0.0.1:${port}/api/snapshot`).then((response) =>
+ response.json(),
+ )) as { units: unknown[]; alerts: { id: string }[] };
+ expect(snapshot.units).toHaveLength(14);
+ expect(snapshot.alerts.length).toBeGreaterThan(0);
+
+ const history = (await fetch(
+ `http://127.0.0.1:${port}/api/units/m-14/history?points=4`,
+ ).then((response) => response.json())) as { samples: unknown[] };
+ expect(history.samples).toHaveLength(4);
+
+ const missing = await fetch(`http://127.0.0.1:${port}/api/units/nope/history`);
+ expect(missing.status).toBe(404);
+
+ const id = snapshot.alerts[0]?.id ?? '';
+ const ack = await fetch(
+ `http://127.0.0.1:${port}/api/alerts/${encodeURIComponent(id)}/ack`,
+ {
+ method: 'POST',
+ },
+ );
+ expect(ack.ok).toBe(true);
+ const next = (await fetch(`http://127.0.0.1:${port}/api/snapshot`).then((response) =>
+ response.json(),
+ )) as { alerts: { id: string }[] };
+ expect(next.alerts.map((alert) => alert.id)).not.toContain(id);
+ });
+
+ it('pushes a snapshot over the socket', async () => {
+ const port = await start();
+ const message = await new Promise<{ type?: string; units?: unknown[] }>(
+ (resolve, reject) => {
+ const socket = new WebSocket(`ws://127.0.0.1:${port}/ws`);
+ const timer = setTimeout(() => {
+ socket.close();
+ reject(new Error('socket timeout'));
+ }, 3_000);
+ socket.on('message', (data) => {
+ clearTimeout(timer);
+ socket.close();
+ resolve(JSON.parse(String(data)) as { type?: string; units?: unknown[] });
+ });
+ socket.on('error', (error) => {
+ clearTimeout(timer);
+ reject(error);
+ });
+ },
+ );
+ expect(message.type).toBe('snapshot');
+ expect(message.units).toHaveLength(14);
+ });
+});
diff --git a/server/simulator.ts b/server/simulator.ts
new file mode 100644
index 0000000..d095a8b
--- /dev/null
+++ b/server/simulator.ts
@@ -0,0 +1,114 @@
+import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
+import { WebSocket, WebSocketServer } from 'ws';
+import { HISTORY_POINTS, createEngine } from '../src/sim/engine';
+
+function writeJson(response: ServerResponse, status: number, body: unknown): void {
+ response.writeHead(status, {
+ 'content-type': 'application/json; charset=utf-8',
+ 'access-control-allow-origin': '*',
+ 'access-control-allow-methods': 'GET,POST,OPTIONS',
+ 'access-control-allow-headers': 'content-type',
+ 'cache-control': 'no-store',
+ });
+ response.end(JSON.stringify(body));
+}
+
+function decodePart(value: string): string | undefined {
+ try {
+ return decodeURIComponent(value);
+ } catch {
+ return undefined;
+ }
+}
+
+function clampPoints(raw: string | null): number {
+ const parsed = Number(raw ?? HISTORY_POINTS);
+ if (!Number.isFinite(parsed)) return HISTORY_POINTS;
+ return Math.min(120, Math.max(2, Math.round(parsed)));
+}
+
+export function createSimulatorServer() {
+ const engine = createEngine();
+
+ const server = createServer((request: IncomingMessage, response: ServerResponse) => {
+ if (request.method === 'OPTIONS') {
+ writeJson(response, 204, {});
+ return;
+ }
+
+ const url = new URL(request.url ?? '/', 'http://127.0.0.1');
+ const { pathname } = url;
+
+ if (request.method === 'GET' && pathname === '/api/health') {
+ writeJson(response, 200, { ok: true });
+ return;
+ }
+
+ if (request.method === 'GET' && pathname === '/api/snapshot') {
+ writeJson(response, 200, engine.snapshot());
+ return;
+ }
+
+ const historyMatch = pathname.match(/^\/api\/units\/([^/]+)\/history$/);
+ if (request.method === 'GET' && historyMatch) {
+ const unitId = decodePart(historyMatch[1] ?? '');
+ if (!unitId) {
+ writeJson(response, 400, { error: 'Bad unit id' });
+ return;
+ }
+ const points = clampPoints(url.searchParams.get('points'));
+ const samples = engine.history(unitId, Date.now(), points);
+ if (samples.length === 0) {
+ writeJson(response, 404, { error: 'Unknown unit' });
+ return;
+ }
+ writeJson(response, 200, { unitId, samples });
+ return;
+ }
+
+ const ackMatch = pathname.match(/^\/api\/alerts\/([^/]+)\/ack$/);
+ if (request.method === 'POST' && ackMatch) {
+ const id = decodePart(ackMatch[1] ?? '');
+ if (!id) {
+ writeJson(response, 400, { error: 'Bad alert id' });
+ return;
+ }
+ engine.acknowledge(id);
+ writeJson(response, 200, { ok: true });
+ return;
+ }
+
+ writeJson(response, 404, { error: 'Not found' });
+ });
+
+ const sockets = new WebSocketServer({ server, path: '/ws' });
+
+ const broadcast = () => {
+ const payload = JSON.stringify({ type: 'tick', ...engine.snapshot() });
+ for (const client of sockets.clients) {
+ if (client.readyState === WebSocket.OPEN) client.send(payload);
+ }
+ };
+
+ sockets.on('connection', (socket) => {
+ socket.send(JSON.stringify({ type: 'snapshot', ...engine.snapshot() }));
+ });
+
+ const timer = setInterval(broadcast, 1000);
+
+ const close = () =>
+ new Promise((resolve, reject) => {
+ clearInterval(timer);
+ for (const client of sockets.clients) client.close();
+ sockets.close();
+ server.close((error) => {
+ if (error && (error as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') {
+ reject(error);
+ return;
+ }
+ resolve();
+ });
+ });
+
+ return { server, close };
+}
diff --git a/src/App.test.tsx b/src/App.test.tsx
new file mode 100644
index 0000000..d038fc5
--- /dev/null
+++ b/src/App.test.tsx
@@ -0,0 +1,52 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { createElement } from 'react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { App } from './App';
+
+vi.mock('./components/MapPanel', async () => {
+ const React = await import('react');
+ return {
+ MapPanel: () => React.createElement('section', { 'aria-label': 'Live map' }),
+ };
+});
+
+function renderApp() {
+ const client = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+ return render(createElement(QueryClientProvider, { client }, createElement(App)));
+}
+
+describe('App', () => {
+ beforeEach(() => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(() => Promise.reject(new Error('offline'))),
+ );
+ });
+
+ it('falls back to the in-browser fleet and filters drones', async () => {
+ const user = userEvent.setup();
+ renderApp();
+
+ expect(
+ await screen.findByRole('option', { name: /M-14/ }, { timeout: 3000 }),
+ ).toBeInTheDocument();
+ expect(screen.getByText('Browser sim')).toBeInTheDocument();
+ expect(screen.getByRole('region', { name: 'Live map' })).toBeInTheDocument();
+
+ await user.click(
+ within(screen.getByRole('group', { name: 'Unit kind' })).getByRole('button', {
+ name: 'Drones',
+ }),
+ );
+ expect(screen.queryByRole('option', { name: /M-14/ })).not.toBeInTheDocument();
+ expect(screen.getByRole('option', { name: /D-12/ })).toBeInTheDocument();
+
+ await user.click(screen.getByRole('option', { name: /D-12/ }));
+ expect(screen.getByRole('region', { name: 'Telemetry' })).toHaveTextContent('D-12');
+ expect(screen.getByRole('img', { name: /Speed in km\/h/ })).toBeInTheDocument();
+ });
+});
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..49aaaeb
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,110 @@
+import { useQuery } from '@tanstack/react-query';
+import { useEffect } from 'react';
+import { AlertsPanel } from './components/AlertsPanel';
+import { FleetList } from './components/FleetList';
+import { MapPanel } from './components/MapPanel';
+import { TelemetryPanel } from './components/TelemetryPanel';
+import { TopBar } from './components/TopBar';
+import { browserEngine } from './data/browserEngine';
+import { useDataMode, useFleetStream } from './data/useFleetLink';
+import { isHistory, isSnapshot } from './guards';
+import { sortUnits } from './filters';
+import { useUiStore } from './store/ui';
+import type { FleetSnapshot } from './types';
+
+async function loadSnapshot(mode: 'live' | 'browser'): Promise {
+ if (mode === 'browser') return browserEngine.snapshot();
+ const response = await fetch('/api/snapshot');
+ if (!response.ok) throw new Error(`Snapshot failed (${response.status})`);
+ const body: unknown = await response.json();
+ if (!isSnapshot(body)) throw new Error('Snapshot payload was not valid');
+ return body;
+}
+
+async function loadHistory(mode: 'live' | 'browser', unitId: string) {
+ if (mode === 'browser') return browserEngine.history(unitId);
+ const response = await fetch(`/api/units/${encodeURIComponent(unitId)}/history?points=48`);
+ if (!response.ok) throw new Error(`History failed (${response.status})`);
+ const body: unknown = await response.json();
+ if (!isHistory(body)) throw new Error('History payload was not valid');
+ return body.samples;
+}
+
+export function App() {
+ useDataMode();
+ useFleetStream();
+
+ const mode = useUiStore((state) => state.dataMode);
+ const linkNote = useUiStore((state) => state.linkNote);
+ const selectedId = useUiStore((state) => state.selectedId);
+ const setSelectedId = useUiStore((state) => state.setSelectedId);
+ const acknowledged = useUiStore((state) => state.acknowledged);
+ const acknowledge = useUiStore((state) => state.acknowledge);
+
+ const fleet = useQuery({
+ queryKey: ['fleet', mode],
+ enabled: mode === 'live' || mode === 'browser',
+ queryFn: () => loadSnapshot(mode === 'live' ? 'live' : 'browser'),
+ });
+
+ const history = useQuery({
+ queryKey: ['history', mode, selectedId],
+ enabled: Boolean(selectedId) && (mode === 'live' || mode === 'browser'),
+ queryFn: () => loadHistory(mode === 'live' ? 'live' : 'browser', selectedId ?? ''),
+ refetchInterval: 5_000,
+ });
+
+ const units = fleet.data?.units ?? [];
+
+ useEffect(() => {
+ if (selectedId) return;
+ const next = fleet.data?.units;
+ if (!next || next.length === 0) return;
+ const first = sortUnits(next)[0];
+ if (first) setSelectedId(first.id);
+ }, [fleet.data, selectedId, setSelectedId]);
+
+ const selected = units.find((unit) => unit.id === selectedId) ?? null;
+ const alerts = (fleet.data?.alerts ?? []).filter((alert) => !acknowledged[alert.id]);
+ const loading = mode === 'detecting' || fleet.isPending;
+
+ const onAcknowledge = (id: string) => {
+ acknowledge(id);
+ if (mode !== 'live') return;
+ void fetch(`/api/alerts/${encodeURIComponent(id)}/ack`, { method: 'POST' }).catch(() => {
+ // The alert stays hidden locally even if the simulator is gone.
+ });
+ };
+
+ return (
+
+
+
+ {
+ void fleet.refetch();
+ }}
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/components/AlertsPanel.test.tsx b/src/components/AlertsPanel.test.tsx
new file mode 100644
index 0000000..23dbfca
--- /dev/null
+++ b/src/components/AlertsPanel.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, expect, it, vi } from 'vitest';
+import type { Alert } from '../types';
+import { AlertsPanel } from './AlertsPanel';
+
+const alert: Alert = {
+ id: 'link:m-09',
+ unitId: 'm-09',
+ callsign: 'M-09',
+ severity: 'warning',
+ code: 'link-degraded',
+ message: 'M-09 telemetry link is intermittent',
+ at: 1_000,
+};
+
+describe('AlertsPanel', () => {
+ it('acknowledges a specific alert', async () => {
+ const user = userEvent.setup();
+ const onAcknowledge = vi.fn();
+ render();
+ expect(screen.getByText('M-09 telemetry link is intermittent')).toBeInTheDocument();
+ expect(screen.getByText(/11s ago/)).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: 'Acknowledge M-09 Link' }));
+ expect(onAcknowledge).toHaveBeenCalledWith('link:m-09');
+ });
+
+ it('shows the quiet state', () => {
+ render( undefined} />);
+ expect(screen.getByText(/No open alerts/)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/AlertsPanel.tsx b/src/components/AlertsPanel.tsx
new file mode 100644
index 0000000..ecccab8
--- /dev/null
+++ b/src/components/AlertsPanel.tsx
@@ -0,0 +1,50 @@
+import { alertCodeLabel, formatAge, severityLabel } from '../format';
+import type { Alert } from '../types';
+
+export function AlertsPanel({
+ alerts,
+ now,
+ onAcknowledge,
+}: {
+ alerts: Alert[];
+ now: number;
+ onAcknowledge: (id: string) => void;
+}) {
+ return (
+
+
+ Alerts
+
+ {alerts.length} open
+
+
+ {alerts.length === 0 ? (
+ No open alerts. Degraded links and low batteries show up here.
+ ) : (
+
+
+ {alerts.map((alert) => (
+ -
+
+ {severityLabel(alert.severity)} · {alertCodeLabel(alert.code)}
+
+ {alert.message}
+
+ {alert.callsign} · {formatAge(alert.at, now)}
+
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/FleetList.test.tsx b/src/components/FleetList.test.tsx
new file mode 100644
index 0000000..8de9570
--- /dev/null
+++ b/src/components/FleetList.test.tsx
@@ -0,0 +1,62 @@
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, expect, it } from 'vitest';
+import { createEngine } from '../sim/engine';
+import { useUiStore } from '../store/ui';
+import { FleetList } from './FleetList';
+
+const snapshot = createEngine().snapshot(1_700_000_000_000);
+
+function renderList() {
+ return render(
+ undefined} />,
+ );
+}
+
+describe('FleetList', () => {
+ it('filters by search, kind, and status', async () => {
+ const user = userEvent.setup();
+ renderList();
+
+ expect(screen.getByRole('option', { name: /M-14/ })).toBeInTheDocument();
+ await user.type(
+ screen.getByRole('searchbox', { name: 'Search callsign, route, or payload' }),
+ 'wharf',
+ );
+ expect(screen.getByRole('option', { name: /M-41/ })).toBeInTheDocument();
+ expect(screen.queryByRole('option', { name: /M-14/ })).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole('button', { name: 'Clear filters' }));
+ await user.click(
+ within(screen.getByRole('group', { name: 'Unit kind' })).getByRole('button', {
+ name: 'Drones',
+ }),
+ );
+ expect(screen.queryByRole('option', { name: /M-14/ })).not.toBeInTheDocument();
+ expect(screen.getByRole('option', { name: /D-12/ })).toBeInTheDocument();
+
+ await user.click(
+ within(screen.getByRole('group', { name: 'Unit kind' })).getByRole('button', {
+ name: 'All',
+ }),
+ );
+ await user.click(
+ within(screen.getByRole('group', { name: 'Unit status' })).getByRole('button', {
+ name: 'Alert',
+ }),
+ );
+ expect(screen.getByRole('option', { name: /M-09/ })).toBeInTheDocument();
+ expect(screen.queryByRole('option', { name: /M-03/ })).not.toBeInTheDocument();
+ });
+
+ it('selects a unit', async () => {
+ const user = userEvent.setup();
+ renderList();
+ await user.click(screen.getByRole('option', { name: /D-12/ }));
+ expect(useUiStore.getState().selectedId).toBe('d-12');
+ expect(screen.getByRole('option', { name: /D-12/ })).toHaveAttribute(
+ 'aria-selected',
+ 'true',
+ );
+ });
+});
diff --git a/src/components/FleetList.tsx b/src/components/FleetList.tsx
new file mode 100644
index 0000000..2c374f1
--- /dev/null
+++ b/src/components/FleetList.tsx
@@ -0,0 +1,203 @@
+import { filterUnits, filtersAreActive, sortUnits } from '../filters';
+import { kindLabel, statusLabel } from '../format';
+import { useUiStore } from '../store/ui';
+import type { FleetUnit, UnitKind, UnitStatus } from '../types';
+import { DroneIcon, GroundIcon } from './icons';
+
+const KINDS: Array<'all' | UnitKind> = ['all', 'ground', 'drone'];
+const STATUSES: Array<'all' | UnitStatus> = ['all', 'enroute', 'idle', 'charging', 'alert'];
+
+function kindChipLabel(kind: 'all' | UnitKind): string {
+ switch (kind) {
+ case 'all':
+ return 'All';
+ case 'ground':
+ return 'Ground';
+ case 'drone':
+ return 'Drones';
+ default: {
+ const unreachable: never = kind;
+ return unreachable;
+ }
+ }
+}
+
+function statusChipLabel(status: 'all' | UnitStatus): string {
+ switch (status) {
+ case 'all':
+ return 'All';
+ case 'enroute':
+ case 'idle':
+ case 'charging':
+ case 'alert':
+ return statusLabel(status);
+ default: {
+ const unreachable: never = status;
+ return unreachable;
+ }
+ }
+}
+
+function meterClass(batteryPct: number): string {
+ if (batteryPct < 12) return 'meter is-critical';
+ if (batteryPct < 20) return 'meter is-low';
+ return 'meter';
+}
+
+export function FleetList({
+ units,
+ loading,
+ error,
+ onRetry,
+}: {
+ units: FleetUnit[];
+ loading: boolean;
+ error: string | null;
+ onRetry: () => void;
+}) {
+ const search = useUiStore((state) => state.search);
+ const kind = useUiStore((state) => state.kind);
+ const status = useUiStore((state) => state.status);
+ const selectedId = useUiStore((state) => state.selectedId);
+ const setSearch = useUiStore((state) => state.setSearch);
+ const setKind = useUiStore((state) => state.setKind);
+ const setStatus = useUiStore((state) => state.setStatus);
+ const setSelectedId = useUiStore((state) => state.setSelectedId);
+ const clearFilters = useUiStore((state) => state.clearFilters);
+
+ const filters = { search, kind, status };
+ const visible = sortUnits(filterUnits(units, filters));
+ const selectionHidden =
+ Boolean(selectedId) && !visible.some((unit) => unit.id === selectedId);
+
+ return (
+
+
+
+
+
+ {KINDS.map((option) => (
+
+ ))}
+
+
+ {STATUSES.map((option) => (
+
+ ))}
+
+ {filtersAreActive(filters) ? (
+
+ ) : null}
+
+ {loading ? (
+
+ ) : error ? (
+
+ ) : visible.length === 0 ? (
+
+
+ {filtersAreActive(filters)
+ ? 'No units match these filters.'
+ : 'No units in the snapshot.'}
+
+ {filtersAreActive(filters) ? (
+
+ ) : null}
+
+ ) : (
+
+ {selectionHidden ? (
+
The selected unit is hidden by the current filters.
+ ) : null}
+
+ {visible.map((unit) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/MapPanel.tsx b/src/components/MapPanel.tsx
new file mode 100644
index 0000000..006fc7f
--- /dev/null
+++ b/src/components/MapPanel.tsx
@@ -0,0 +1,162 @@
+import L from 'leaflet';
+import { useEffect, useRef, useState } from 'react';
+import {
+ MapContainer,
+ Marker,
+ Popup,
+ Rectangle,
+ TileLayer,
+ Tooltip,
+ useMap,
+} from 'react-leaflet';
+import { kindLabel, statusLabel } from '../format';
+import { preferenceFromMeta, readMetaContent, resolveScheme } from '../theme';
+import type { FleetUnit, RestrictedZone } from '../types';
+import { useSystemScheme } from '../useSystemScheme';
+
+function useResolvedScheme(): 'light' | 'dark' {
+ const system = useSystemScheme();
+ const [meta, setMeta] = useState(readMetaContent);
+
+ useEffect(() => {
+ const element = document.querySelector('meta[name="color-scheme"]');
+ if (!element) return;
+ const observer = new MutationObserver(() => setMeta(readMetaContent()));
+ observer.observe(element, { attributes: true, attributeFilter: ['content'] });
+ return () => observer.disconnect();
+ }, []);
+
+ return resolveScheme(preferenceFromMeta(meta), system);
+}
+
+function pinIcon(unit: FleetUnit, selected: boolean): L.DivIcon {
+ const selectedClass = selected ? ' is-selected' : '';
+ return L.divIcon({
+ className: 'pin-wrap',
+ html: ``,
+ iconSize: [22, 22],
+ iconAnchor: [11, 11],
+ });
+}
+
+function FitOnce({ units }: { units: FleetUnit[] }) {
+ const map = useMap();
+ const fitted = useRef(false);
+
+ useEffect(() => {
+ if (fitted.current || units.length === 0) return;
+ const bounds = L.latLngBounds(units.map((unit) => [unit.lat, unit.lng]));
+ map.fitBounds(bounds.pad(0.18));
+ fitted.current = true;
+ }, [map, units]);
+
+ return null;
+}
+
+function FollowSelection({ unit }: { unit: FleetUnit | null }) {
+ const map = useMap();
+ const skipInitial = useRef(true);
+ const lastId = useRef(null);
+
+ useEffect(() => {
+ if (!unit) return;
+ if (skipInitial.current) {
+ skipInitial.current = false;
+ lastId.current = unit.id;
+ return;
+ }
+ if (lastId.current === unit.id) return;
+ lastId.current = unit.id;
+ const target: L.LatLngExpression = [unit.lat, unit.lng];
+ const zoom = Math.max(map.getZoom(), 13);
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ if (reduce) map.setView(target, zoom);
+ else map.flyTo(target, zoom, { duration: 0.55 });
+ }, [map, unit]);
+
+ return null;
+}
+
+const TILES = {
+ light: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
+ dark: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
+} as const;
+
+export function MapPanel({
+ units,
+ zone,
+ selectedId,
+ onSelect,
+}: {
+ units: FleetUnit[];
+ zone: RestrictedZone | null;
+ selectedId: string | null;
+ onSelect: (id: string) => void;
+}) {
+ const scheme = useResolvedScheme();
+ const selected = units.find((unit) => unit.id === selectedId) ?? null;
+
+ return (
+
+
+ Simulated positions on an OpenStreetMap basemap.
+
+
+
+ {zone ? (
+
+ {zone.name} · restricted airspace
+
+ ) : null}
+
+
+ {units.map((unit) => (
+ onSelect(unit.id) }}
+ >
+
+ {unit.callsign}
+
+ {kindLabel(unit.kind)} · {statusLabel(unit.status)}
+
+ {unit.speedKph} km/h · {unit.batteryPct}% battery
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/SparkChart.test.tsx b/src/components/SparkChart.test.tsx
new file mode 100644
index 0000000..d37dd3c
--- /dev/null
+++ b/src/components/SparkChart.test.tsx
@@ -0,0 +1,36 @@
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { SparkChart } from './SparkChart';
+
+describe('SparkChart', () => {
+ it('draws a line and names the latest reading', () => {
+ const { container } = render(
+ ,
+ );
+ expect(
+ screen.getByRole('img', { name: /Speed in km\/h, Last 4 min, latest 15/ }),
+ ).toBeInTheDocument();
+ expect(container.querySelector('.chart__line')?.getAttribute('d')).toMatch(/^M/);
+ expect(screen.getByText('20')).toBeInTheDocument();
+ expect(screen.getByText('10')).toBeInTheDocument();
+ });
+
+ it('explains a missing series', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('No samples')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/SparkChart.tsx b/src/components/SparkChart.tsx
new file mode 100644
index 0000000..990cfa3
--- /dev/null
+++ b/src/components/SparkChart.tsx
@@ -0,0 +1,93 @@
+function domain(values: number[]): { lo: number; hi: number } {
+ const lo = Math.min(...values);
+ const hi = Math.max(...values);
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo === hi) {
+ return { lo: lo - 1, hi: hi + 1 };
+ }
+ const pad = (hi - lo) * 0.12;
+ return { lo: lo - pad, hi: hi + pad };
+}
+
+function formatReading(value: number): string {
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
+}
+
+export function SparkChart({
+ label,
+ unit,
+ values,
+ rangeLabel,
+ tone,
+}: {
+ label: string;
+ unit: string;
+ values: number[];
+ rangeLabel: string;
+ tone: 'speed' | 'battery' | 'altitude';
+}) {
+ const width = 320;
+ const height = 120;
+ const padX = 36;
+ const padTop = 12;
+ const padBottom = 18;
+
+ if (values.length < 2) {
+ return (
+
+
+
+ {label} ({unit})
+
+ —
+
+ No samples
+
+ );
+ }
+
+ const { lo, hi } = domain(values);
+ const span = hi - lo || 1;
+ const latest = values[values.length - 1] ?? 0;
+ const dataLo = Math.min(...values);
+ const dataHi = Math.max(...values);
+ const coords = values.map((value, index) => {
+ const x = padX + (index / (values.length - 1)) * (width - padX - 8);
+ const y = padTop + (1 - (value - lo) / span) * (height - padTop - padBottom);
+ return [x, y] as const;
+ });
+ const line = coords
+ .map(([x, y], index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`)
+ .join(' ');
+ const last = coords[coords.length - 1] ?? [padX, height];
+ const first = coords[0] ?? [padX, height];
+ const baseline = height - padBottom;
+ const area = `${line} L${last[0].toFixed(1)} ${baseline} L${first[0].toFixed(1)} ${baseline} Z`;
+
+ return (
+
+
+
+ {label} ({unit})
+
+
+ {formatReading(latest)} {unit}
+
+
+
+ {rangeLabel}
+
+ );
+}
diff --git a/src/components/TelemetryPanel.tsx b/src/components/TelemetryPanel.tsx
new file mode 100644
index 0000000..ad153da
--- /dev/null
+++ b/src/components/TelemetryPanel.tsx
@@ -0,0 +1,114 @@
+import {
+ formatCoord,
+ formatHeading,
+ historyWindowLabel,
+ kindLabel,
+ statusLabel,
+} from '../format';
+import type { FleetUnit, TelemetrySample } from '../types';
+import { SparkChart } from './SparkChart';
+
+function HeadingDial({ heading }: { heading: number }) {
+ return (
+
+ );
+}
+
+export function TelemetryPanel({
+ unit,
+ samples,
+ loading,
+}: {
+ unit: FleetUnit | null;
+ samples: TelemetrySample[];
+ loading: boolean;
+}) {
+ const rangeLabel = historyWindowLabel();
+
+ return (
+
+
+ {!unit ? (
+ Select a unit to read speed, battery, and altitude.
+ ) : (
+
+
+
+
+ {unit.callsign}
+
+ {statusLabel(unit.status)}
+
+
+
+ {kindLabel(unit.kind)} · {unit.routeName} · {unit.payload}
+
+
+ {unit.desk} · {formatCoord(unit.lat)}, {formatCoord(unit.lng)}
+
+
+
+
+
+
+
- Speed
+ - {unit.speedKph} km/h
+
+
+
- Battery
+ -
+ {unit.batteryPct}%
+
+
+
+
- Altitude
+ - {unit.altitudeM} m
+
+
+
- Heading
+ - {formatHeading(unit.heading)}
+
+
+ {loading && samples.length === 0 ? (
+
Loading history…
+ ) : (
+
+ sample.speedKph)}
+ />
+ sample.batteryPct)}
+ />
+ sample.altitudeM)}
+ />
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/src/components/ThemeControl.test.tsx b/src/components/ThemeControl.test.tsx
new file mode 100644
index 0000000..352ff7b
--- /dev/null
+++ b/src/components/ThemeControl.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, expect, it } from 'vitest';
+import { ThemeControl } from './ThemeControl';
+
+describe('ThemeControl', () => {
+ it('pins the document to dark', async () => {
+ const meta = document.createElement('meta');
+ meta.setAttribute('name', 'color-scheme');
+ meta.setAttribute('content', 'light dark');
+ document.head.append(meta);
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole('button', { name: 'Dark' }));
+ expect(meta.getAttribute('content')).toBe('dark');
+ expect(screen.getByRole('button', { name: 'Dark' })).toHaveAttribute(
+ 'aria-pressed',
+ 'true',
+ );
+ expect(screen.getByRole('button', { name: /System, currently/ })).toHaveAttribute(
+ 'aria-pressed',
+ 'false',
+ );
+ meta.remove();
+ });
+});
diff --git a/src/components/ThemeControl.tsx b/src/components/ThemeControl.tsx
new file mode 100644
index 0000000..6093f97
--- /dev/null
+++ b/src/components/ThemeControl.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useState } from 'react';
+import { applyPreference, preferenceFromStorage, type SchemePreference } from '../theme';
+import { useSystemScheme } from '../useSystemScheme';
+
+function storedPreference(): SchemePreference {
+ try {
+ return preferenceFromStorage(localStorage.getItem('color-scheme'));
+ } catch {
+ return 'system';
+ }
+}
+
+const OPTIONS: SchemePreference[] = ['system', 'light', 'dark'];
+
+function optionLabel(option: SchemePreference): string {
+ switch (option) {
+ case 'system':
+ return 'System';
+ case 'light':
+ return 'Light';
+ case 'dark':
+ return 'Dark';
+ default: {
+ const unreachable: never = option;
+ return unreachable;
+ }
+ }
+}
+
+export function ThemeControl() {
+ const system = useSystemScheme();
+ const [preference, setPreference] = useState(storedPreference);
+
+ useEffect(() => {
+ const onStorage = (event: StorageEvent) => {
+ if (event.key !== 'color-scheme') return;
+ const next = preferenceFromStorage(event.newValue);
+ applyPreference(next);
+ setPreference(next);
+ };
+ window.addEventListener('storage', onStorage);
+ return () => window.removeEventListener('storage', onStorage);
+ }, []);
+
+ const choose = (next: SchemePreference) => {
+ applyPreference(next);
+ setPreference(next);
+ };
+
+ return (
+
+ {OPTIONS.map((option) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx
new file mode 100644
index 0000000..03f10ed
--- /dev/null
+++ b/src/components/TopBar.tsx
@@ -0,0 +1,77 @@
+import type { DataMode, FleetUnit } from '../types';
+import { Mark } from './icons';
+import { ThemeControl } from './ThemeControl';
+
+function modeLabel(mode: DataMode): string {
+ switch (mode) {
+ case 'detecting':
+ return 'Linking';
+ case 'live':
+ return 'Live socket';
+ case 'browser':
+ return 'Browser sim';
+ default: {
+ const unreachable: never = mode;
+ return unreachable;
+ }
+ }
+}
+
+export function TopBar({
+ mode,
+ linkNote,
+ units,
+ alertCount,
+}: {
+ mode: DataMode;
+ linkNote: string;
+ units: FleetUnit[];
+ alertCount: number;
+}) {
+ const moving = units.filter((unit) => unit.speedKph > 1).length;
+ const averageBattery =
+ units.length === 0
+ ? null
+ : Math.round(units.reduce((sum, unit) => sum + unit.batteryPct, 0) / units.length);
+
+ return (
+
+
+ Skip to fleet
+
+
+
+
+
Meridian
+
Fleet operations · portfolio demo
+
+
+
+
+
- Units
+ - {units.length || '—'}
+
+
+
- Moving
+ - {units.length ? moving : '—'}
+
+
+
- Alerts
+ - {units.length ? alertCount : '—'}
+
+
+
- Avg battery
+ - {averageBattery === null ? '—' : `${averageBattery}%`}
+
+
+
+
+
+ {modeLabel(mode)}
+ {linkNote}
+
+
+
+
+ );
+}
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
new file mode 100644
index 0000000..2478e81
--- /dev/null
+++ b/src/components/icons.tsx
@@ -0,0 +1,40 @@
+export function Mark() {
+ return (
+
+ );
+}
+
+export function GroundIcon() {
+ return (
+
+ );
+}
+
+export function DroneIcon() {
+ return (
+
+ );
+}
diff --git a/src/data/browserEngine.ts b/src/data/browserEngine.ts
new file mode 100644
index 0000000..4e0584e
--- /dev/null
+++ b/src/data/browserEngine.ts
@@ -0,0 +1,4 @@
+import { createEngine } from '../sim/engine';
+
+/** One simulator for the tab, used when the Node server is absent. */
+export const browserEngine = createEngine();
diff --git a/src/data/useFleetLink.ts b/src/data/useFleetLink.ts
new file mode 100644
index 0000000..24d9734
--- /dev/null
+++ b/src/data/useFleetLink.ts
@@ -0,0 +1,101 @@
+import { useQueryClient } from '@tanstack/react-query';
+import { useEffect } from 'react';
+import { isSnapshot } from '../guards';
+import { useUiStore } from '../store/ui';
+import type { FleetSnapshot } from '../types';
+import { browserEngine } from './browserEngine';
+
+export const forceBrowser = import.meta.env.VITE_FORCE_BROWSER === 'true';
+
+function socketUrl(): string {
+ if (import.meta.env.VITE_WS_URL) return import.meta.env.VITE_WS_URL;
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
+ return `${protocol}://${window.location.host}/ws`;
+}
+
+export function useDataMode(): void {
+ const setLink = useUiStore((state) => state.setLink);
+
+ useEffect(() => {
+ if (forceBrowser) {
+ setLink('browser', 'This build runs the simulator in the browser.');
+ return;
+ }
+
+ let active = true;
+ const controller = new AbortController();
+ const timer = window.setTimeout(() => controller.abort(), 700);
+ fetch('/api/health', { signal: controller.signal })
+ .then((response) => {
+ if (!active) return;
+ if (response.ok) {
+ setLink('live', 'Connected to the local simulator socket.');
+ return;
+ }
+ setLink('browser', 'Simulator server did not respond. Running in this tab.');
+ })
+ .catch(() => {
+ if (!active) return;
+ setLink('browser', 'Simulator server is not reachable. Running in this tab.');
+ })
+ .finally(() => window.clearTimeout(timer));
+
+ return () => {
+ active = false;
+ window.clearTimeout(timer);
+ controller.abort();
+ };
+ }, [setLink]);
+}
+
+export function useFleetStream(): void {
+ const mode = useUiStore((state) => state.dataMode);
+ const setLink = useUiStore((state) => state.setLink);
+ const queryClient = useQueryClient();
+
+ useEffect(() => {
+ if (mode === 'browser') {
+ const id = window.setInterval(() => {
+ queryClient.setQueryData(['fleet', 'browser'], browserEngine.snapshot());
+ }, 1000);
+ return () => window.clearInterval(id);
+ }
+
+ if (mode !== 'live') return;
+
+ let stopped = false;
+ let socket: WebSocket | null = null;
+ let retries = 0;
+ let retryTimer = 0;
+
+ const connect = () => {
+ socket = new WebSocket(socketUrl());
+ socket.onmessage = (event) => {
+ try {
+ const payload: unknown = JSON.parse(String(event.data));
+ if (!isSnapshot(payload)) return;
+ retries = 0;
+ queryClient.setQueryData(['fleet', 'live'], payload);
+ } catch {
+ // Ignore malformed frames. The next tick replaces them.
+ }
+ };
+ socket.onclose = () => {
+ if (stopped) return;
+ retries += 1;
+ if (retries >= 2) {
+ setLink('browser', 'Live socket closed. Showing the in-browser simulator.');
+ return;
+ }
+ retryTimer = window.setTimeout(connect, 400);
+ };
+ };
+
+ connect();
+ return () => {
+ stopped = true;
+ window.clearTimeout(retryTimer);
+ socket?.close();
+ };
+ }, [mode, queryClient, setLink]);
+}
diff --git a/src/filters.test.ts b/src/filters.test.ts
new file mode 100644
index 0000000..d6e445c
--- /dev/null
+++ b/src/filters.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect, it } from 'vitest';
+import { filterUnits, sortUnits } from './filters';
+import type { FleetUnit } from './types';
+
+function unit(
+ partial: Partial & Pick,
+): FleetUnit {
+ return {
+ kind: 'ground',
+ lat: 0,
+ lng: 0,
+ heading: 0,
+ speedKph: 10,
+ batteryPct: 50,
+ altitudeM: 0,
+ payload: 'crates',
+ routeName: 'loop',
+ desk: 'desk',
+ link: 'nominal',
+ updatedAt: 0,
+ ...partial,
+ };
+}
+
+const units = [
+ unit({
+ id: 'a',
+ callsign: 'M-14',
+ status: 'enroute',
+ batteryPct: 10,
+ routeName: 'Embarcadero',
+ }),
+ unit({
+ id: 'b',
+ callsign: 'D-12',
+ status: 'alert',
+ kind: 'drone',
+ batteryPct: 90,
+ payload: 'lidar',
+ }),
+ unit({ id: 'c', callsign: 'M-03', status: 'idle', batteryPct: 40, routeName: 'Yard' }),
+];
+
+describe('filters', () => {
+ it('matches callsign, route, and payload without case', () => {
+ expect(
+ filterUnits(units, { search: 'embar', kind: 'all', status: 'all' }).map((u) => u.id),
+ ).toEqual(['a']);
+ expect(
+ filterUnits(units, { search: 'LIDAR', kind: 'all', status: 'all' }).map((u) => u.id),
+ ).toEqual(['b']);
+ });
+
+ it('filters by kind and status', () => {
+ expect(
+ filterUnits(units, { search: '', kind: 'drone', status: 'all' }).map((u) => u.id),
+ ).toEqual(['b']);
+ expect(
+ filterUnits(units, { search: '', kind: 'all', status: 'idle' }).map((u) => u.id),
+ ).toEqual(['c']);
+ });
+
+ it('sorts alerts ahead of healthier units', () => {
+ expect(sortUnits(units).map((u) => u.callsign)).toEqual(['D-12', 'M-03', 'M-14']);
+ });
+});
diff --git a/src/filters.ts b/src/filters.ts
new file mode 100644
index 0000000..7ee73d4
--- /dev/null
+++ b/src/filters.ts
@@ -0,0 +1,51 @@
+import type { FleetUnit, UnitKind, UnitStatus } from './types';
+
+export interface FleetFilters {
+ search: string;
+ kind: 'all' | UnitKind;
+ status: 'all' | UnitStatus;
+}
+
+export function filterUnits(units: FleetUnit[], filters: FleetFilters): FleetUnit[] {
+ const query = filters.search.trim().toLowerCase();
+ return units.filter((unit) => {
+ if (filters.kind !== 'all' && unit.kind !== filters.kind) return false;
+ if (filters.status !== 'all' && unit.status !== filters.status) return false;
+ if (!query) return true;
+ return (
+ unit.callsign.toLowerCase().includes(query) ||
+ unit.routeName.toLowerCase().includes(query) ||
+ unit.payload.toLowerCase().includes(query)
+ );
+ });
+}
+
+function statusRank(status: UnitStatus): number {
+ switch (status) {
+ case 'alert':
+ return 0;
+ case 'charging':
+ return 1;
+ case 'idle':
+ return 2;
+ case 'enroute':
+ return 3;
+ default: {
+ const unreachable: never = status;
+ return unreachable;
+ }
+ }
+}
+
+export function sortUnits(units: FleetUnit[]): FleetUnit[] {
+ return [...units].sort((a, b) => {
+ const byStatus = statusRank(a.status) - statusRank(b.status);
+ if (byStatus !== 0) return byStatus;
+ if (a.batteryPct !== b.batteryPct) return a.batteryPct - b.batteryPct;
+ return a.callsign.localeCompare(b.callsign);
+ });
+}
+
+export function filtersAreActive(filters: FleetFilters): boolean {
+ return filters.search.trim() !== '' || filters.kind !== 'all' || filters.status !== 'all';
+}
diff --git a/src/format.test.ts b/src/format.test.ts
new file mode 100644
index 0000000..4106eeb
--- /dev/null
+++ b/src/format.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest';
+import { alertCodeLabel, formatAge, formatHeading, statusLabel } from './format';
+
+describe('format', () => {
+ it('formats ages in short buckets', () => {
+ expect(formatAge(0, 0)).toBe('just now');
+ expect(formatAge(0, 9_000)).toBe('just now');
+ expect(formatAge(0, 12_000)).toBe('12s ago');
+ expect(formatAge(0, 90_000)).toBe('1m ago');
+ expect(formatAge(0, 7_200_000)).toBe('2h ago');
+ });
+
+ it('pads headings and wraps negative degrees', () => {
+ expect(formatHeading(0)).toBe('000°');
+ expect(formatHeading(90)).toBe('090°');
+ expect(formatHeading(-10)).toBe('350°');
+ expect(formatHeading(359.6)).toBe('000°');
+ });
+
+ it('labels every status and alert code', () => {
+ expect(statusLabel('enroute')).toBe('En route');
+ expect(alertCodeLabel('restricted-zone')).toBe('Airspace');
+ });
+});
diff --git a/src/format.ts b/src/format.ts
new file mode 100644
index 0000000..6fd012f
--- /dev/null
+++ b/src/format.ts
@@ -0,0 +1,85 @@
+import type { AlertCode, Severity, UnitKind, UnitStatus } from './types';
+import { HISTORY_POINTS, HISTORY_STEP_MS } from './sim/engine';
+
+export function statusLabel(status: UnitStatus): string {
+ switch (status) {
+ case 'enroute':
+ return 'En route';
+ case 'idle':
+ return 'Idle';
+ case 'charging':
+ return 'Charging';
+ case 'alert':
+ return 'Alert';
+ default: {
+ const unreachable: never = status;
+ return unreachable;
+ }
+ }
+}
+
+export function kindLabel(kind: UnitKind): string {
+ switch (kind) {
+ case 'ground':
+ return 'Ground';
+ case 'drone':
+ return 'Drone';
+ default: {
+ const unreachable: never = kind;
+ return unreachable;
+ }
+ }
+}
+
+export function severityLabel(severity: Severity): string {
+ switch (severity) {
+ case 'warning':
+ return 'Warning';
+ case 'critical':
+ return 'Critical';
+ default: {
+ const unreachable: never = severity;
+ return unreachable;
+ }
+ }
+}
+
+export function alertCodeLabel(code: AlertCode): string {
+ switch (code) {
+ case 'low-battery':
+ return 'Battery';
+ case 'restricted-zone':
+ return 'Airspace';
+ case 'link-degraded':
+ return 'Link';
+ default: {
+ const unreachable: never = code;
+ return unreachable;
+ }
+ }
+}
+
+export function formatAge(at: number, now: number): string {
+ const seconds = Math.max(0, Math.round((now - at) / 1000));
+ if (seconds < 10) return 'just now';
+ if (seconds < 60) return `${seconds}s ago`;
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.floor(minutes / 60);
+ return `${hours}h ago`;
+}
+
+export function formatHeading(deg: number): string {
+ const normalized = ((deg % 360) + 360) % 360;
+ const rounded = Math.round(normalized) % 360;
+ return `${rounded.toString().padStart(3, '0')}°`;
+}
+
+export function formatCoord(value: number): string {
+ return value.toFixed(5);
+}
+
+export function historyWindowLabel(points = HISTORY_POINTS, stepMs = HISTORY_STEP_MS): string {
+ const minutes = Math.max(1, Math.round((points * stepMs) / 60_000));
+ return `Last ${minutes} min`;
+}
diff --git a/src/guards.test.ts b/src/guards.test.ts
new file mode 100644
index 0000000..701171e
--- /dev/null
+++ b/src/guards.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest';
+import { isSnapshot } from './guards';
+
+describe('isSnapshot', () => {
+ it('accepts a payload with units, alerts, time, and a zone', () => {
+ expect(
+ isSnapshot({
+ units: [],
+ alerts: [],
+ serverTime: 1,
+ zone: { name: 'South Basin', south: 1, west: 2, north: 3, east: 4 },
+ type: 'tick',
+ }),
+ ).toBe(true);
+ expect(isSnapshot({ units: [] })).toBe(false);
+ });
+});
diff --git a/src/guards.ts b/src/guards.ts
new file mode 100644
index 0000000..18bc86c
--- /dev/null
+++ b/src/guards.ts
@@ -0,0 +1,29 @@
+import type { FleetSnapshot, RestrictedZone, TelemetrySample } from './types';
+
+export function isZone(value: unknown): value is RestrictedZone {
+ if (typeof value !== 'object' || value === null) return false;
+ const zone = value as Partial;
+ return (
+ typeof zone.name === 'string' &&
+ typeof zone.south === 'number' &&
+ typeof zone.west === 'number' &&
+ typeof zone.north === 'number' &&
+ typeof zone.east === 'number'
+ );
+}
+
+export function isSnapshot(value: unknown): value is FleetSnapshot {
+ if (typeof value !== 'object' || value === null) return false;
+ const record = value as Partial;
+ return (
+ Array.isArray(record.units) &&
+ Array.isArray(record.alerts) &&
+ typeof record.serverTime === 'number' &&
+ isZone(record.zone)
+ );
+}
+
+export function isHistory(value: unknown): value is { samples: TelemetrySample[] } {
+ if (typeof value !== 'object' || value === null) return false;
+ return Array.isArray((value as { samples?: unknown }).samples);
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..dad29ab
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,24 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+import 'leaflet/dist/leaflet.css';
+import './styles.css';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
+
+const root = document.getElementById('root');
+if (!root) throw new Error('Missing #root');
+
+// react-leaflet 4 throws "map container is already initialized" under StrictMode.
+createRoot(root).render(
+
+
+ ,
+);
diff --git a/src/sim/engine.test.ts b/src/sim/engine.test.ts
new file mode 100644
index 0000000..3516534
--- /dev/null
+++ b/src/sim/engine.test.ts
@@ -0,0 +1,103 @@
+import { describe, expect, it } from 'vitest';
+import type { FleetUnit } from '../types';
+import { HISTORY_POINTS, createEngine, deriveAlerts, projectUnit } from './engine';
+import { blueprintById, ROSTER } from './roster';
+import { inZone } from './geo';
+import { RESTRICTED_ZONE } from './roster';
+
+const NOW = 1_700_000_000_000;
+
+function unit(partial: Partial & Pick): FleetUnit {
+ return {
+ callsign: partial.id.toUpperCase(),
+ status: 'enroute',
+ lat: 37.77,
+ lng: -122.42,
+ heading: 0,
+ speedKph: 30,
+ batteryPct: 80,
+ altitudeM: 0,
+ payload: 'payload',
+ routeName: 'route',
+ desk: 'desk',
+ link: 'nominal',
+ updatedAt: NOW,
+ ...partial,
+ };
+}
+
+describe('fleet engine', () => {
+ it('projects every roster unit into a finite position', () => {
+ const snapshot = createEngine().snapshot(NOW);
+ expect(snapshot.units).toHaveLength(ROSTER.length);
+ for (const fleetUnit of snapshot.units) {
+ expect(Number.isFinite(fleetUnit.lat)).toBe(true);
+ expect(Number.isFinite(fleetUnit.lng)).toBe(true);
+ expect(fleetUnit.batteryPct).toBeGreaterThanOrEqual(0);
+ expect(fleetUnit.batteryPct).toBeLessThanOrEqual(100);
+ }
+ });
+
+ it('keeps the yard mule parked and the degraded units in alert', () => {
+ const mule = blueprintById('m-03');
+ const degraded = blueprintById('m-09');
+ if (!mule || !degraded) throw new Error('missing blueprint');
+ expect(projectUnit(mule, NOW).speedKph).toBe(0);
+ expect(projectUnit(mule, NOW).status).toBe('idle');
+ expect(projectUnit(degraded, NOW).status).toBe('alert');
+ expect(projectUnit(degraded, NOW).link).toBe('degraded');
+ });
+
+ it('sends the basin drone through the restricted zone', () => {
+ const drone = blueprintById('d-15');
+ if (!drone) throw new Error('missing drone');
+ let hits = 0;
+ for (let t = NOW; t < NOW + 10 * 60_000; t += 2_000) {
+ const projected = projectUnit(drone, t);
+ if (inZone(projected.lat, projected.lng, RESTRICTED_ZONE)) hits += 1;
+ }
+ expect(hits).toBeGreaterThan(0);
+ });
+
+ it('raises link and battery alerts for a degraded unit and keeps them acknowledged', () => {
+ const engine = createEngine();
+ const first = engine.snapshot(NOW);
+ const link = first.alerts.find((alert) => alert.id === 'link:m-09');
+ const battery = first.alerts.find((alert) => alert.id === 'battery:d-02');
+ expect(link?.severity).toBe('warning');
+ expect(battery?.severity).toBe('critical');
+
+ engine.acknowledge('link:m-09');
+ const second = engine.snapshot(NOW + 5_000);
+ expect(second.alerts.some((alert) => alert.id === 'link:m-09')).toBe(false);
+ expect(second.alerts.find((alert) => alert.id === 'link:m-09')).toBeUndefined();
+ expect(second.alerts.find((alert) => alert.id === 'link:m-09')?.at).toBeUndefined();
+ expect(link && second.alerts.find((alert) => alert.id === 'battery:d-02')?.at).toBe(
+ battery?.at,
+ );
+ });
+
+ it('does not flag a ground vehicle inside the airspace box', () => {
+ const alerts = deriveAlerts(
+ [
+ unit({
+ id: 'truck',
+ kind: 'ground',
+ lat: 37.752,
+ lng: -122.39,
+ altitudeM: 0,
+ }),
+ ],
+ new Map(),
+ NOW,
+ );
+ expect(alerts.map((alert) => alert.code)).not.toContain('restricted-zone');
+ });
+
+ it('returns a time-ordered history window', () => {
+ const samples = createEngine().history('d-12', NOW, HISTORY_POINTS);
+ expect(samples).toHaveLength(HISTORY_POINTS);
+ expect(samples[0]?.t).toBeLessThan(samples[samples.length - 1]?.t ?? 0);
+ expect(samples[samples.length - 1]?.t).toBe(NOW);
+ });
+});
diff --git a/src/sim/engine.ts b/src/sim/engine.ts
new file mode 100644
index 0000000..9cef3f1
--- /dev/null
+++ b/src/sim/engine.ts
@@ -0,0 +1,300 @@
+import type { Alert, FleetSnapshot, FleetUnit, TelemetrySample, UnitBlueprint } from '../types';
+import { buildRoute, inZone, pointAlong, type RouteGeom } from './geo';
+import { blueprintById, RESTRICTED_ZONE, ROSTER } from './roster';
+
+export const HISTORY_POINTS = 48;
+export const HISTORY_STEP_MS = 5_000;
+
+const routeCache = new Map();
+
+function geometryFor(blueprint: UnitBlueprint): RouteGeom {
+ const cached = routeCache.get(blueprint.id);
+ if (cached) return cached;
+ const built = buildRoute(blueprint.route);
+ routeCache.set(blueprint.id, built);
+ return built;
+}
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.min(max, Math.max(min, value));
+}
+
+export function batteryAt(blueprint: UnitBlueprint, now: number): number {
+ switch (blueprint.behavior) {
+ case 'idle': {
+ const wobble = 0.5 + 0.5 * Math.sin(now / 20_000);
+ return 74 + Math.round(4 * wobble);
+ }
+ case 'charging': {
+ const phase =
+ ((now + blueprint.phaseMs) % blueprint.batteryPeriodMs) / blueprint.batteryPeriodMs;
+ return clamp(Math.round(22 + phase * 70), 0, 100);
+ }
+ case 'degraded': {
+ const wobble = 0.5 + 0.5 * Math.sin((now + blueprint.phaseMs) / 8_000);
+ return clamp(Math.round(6 + wobble * 5), 0, 100);
+ }
+ case 'patrol': {
+ const phase =
+ ((now + blueprint.phaseMs) % blueprint.batteryPeriodMs) / blueprint.batteryPeriodMs;
+ const wave = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2);
+ return clamp(Math.round(14 + wave * 84), 0, 100);
+ }
+ default: {
+ const unreachable: never = blueprint.behavior;
+ return unreachable;
+ }
+ }
+}
+
+export function speedAt(blueprint: UnitBlueprint, now: number): number {
+ switch (blueprint.behavior) {
+ case 'idle':
+ case 'charging':
+ return 0;
+ case 'degraded':
+ case 'patrol': {
+ const factor = blueprint.behavior === 'degraded' ? 0.35 : 1;
+ const wave = 0.86 + 0.14 * Math.sin((now + blueprint.phaseMs) / 7_000);
+ return Math.round(blueprint.cruiseKph * factor * wave);
+ }
+ default: {
+ const unreachable: never = blueprint.behavior;
+ return unreachable;
+ }
+ }
+}
+
+export function altitudeAt(blueprint: UnitBlueprint, now: number): number {
+ if (blueprint.kind === 'ground') return 0;
+ switch (blueprint.behavior) {
+ case 'idle':
+ case 'charging':
+ return 0;
+ case 'degraded':
+ case 'patrol': {
+ const wave = Math.sin((now + blueprint.phaseMs) / 9_000);
+ return Math.round(blueprint.altitudeBase + blueprint.altitudeAmp * wave);
+ }
+ default: {
+ const unreachable: never = blueprint.behavior;
+ return unreachable;
+ }
+ }
+}
+
+export function projectUnit(blueprint: UnitBlueprint, now: number): FleetUnit {
+ const batteryPct = batteryAt(blueprint, now);
+ const home = blueprint.route[0] ?? { lat: 37.77, lng: -122.42 };
+ let lat = home.lat;
+ let lng = home.lng;
+ let heading = 0;
+
+ switch (blueprint.behavior) {
+ case 'idle':
+ case 'charging':
+ break;
+ case 'degraded':
+ case 'patrol': {
+ // Cruise speed keeps the loop smooth. The readout speed wobbles around it.
+ const factor = blueprint.behavior === 'degraded' ? 0.35 : 1;
+ const metersPerSecond = (blueprint.cruiseKph * factor * 1000) / 3600;
+ const distance = metersPerSecond * ((now + blueprint.phaseMs) / 1000);
+ const point = pointAlong(geometryFor(blueprint), distance);
+ lat = point.lat;
+ lng = point.lng;
+ heading = point.heading;
+ break;
+ }
+ default: {
+ const unreachable: never = blueprint.behavior;
+ throw new Error(`Unknown behavior ${String(unreachable)}`);
+ }
+ }
+
+ let status: FleetUnit['status'];
+ switch (blueprint.behavior) {
+ case 'charging':
+ status = 'charging';
+ break;
+ case 'idle':
+ status = batteryPct < 20 ? 'alert' : 'idle';
+ break;
+ case 'degraded':
+ status = 'alert';
+ break;
+ case 'patrol':
+ status = batteryPct < 20 ? 'alert' : 'enroute';
+ break;
+ default: {
+ const unreachable: never = blueprint.behavior;
+ throw new Error(`Unknown behavior ${String(unreachable)}`);
+ }
+ }
+
+ if (
+ blueprint.kind === 'drone' &&
+ blueprint.behavior !== 'charging' &&
+ blueprint.behavior !== 'idle' &&
+ inZone(lat, lng, RESTRICTED_ZONE)
+ ) {
+ status = 'alert';
+ }
+
+ return {
+ id: blueprint.id,
+ callsign: blueprint.callsign,
+ kind: blueprint.kind,
+ status,
+ lat,
+ lng,
+ heading,
+ speedKph: speedAt(blueprint, now),
+ batteryPct,
+ altitudeM: altitudeAt(blueprint, now),
+ payload: blueprint.payload,
+ routeName: blueprint.routeName,
+ desk: blueprint.desk,
+ link: blueprint.behavior === 'degraded' ? 'degraded' : 'nominal',
+ updatedAt: now,
+ };
+}
+
+export function historyFor(
+ blueprint: UnitBlueprint,
+ now: number,
+ points = HISTORY_POINTS,
+ stepMs = HISTORY_STEP_MS,
+): TelemetrySample[] {
+ const samples: TelemetrySample[] = [];
+ for (let index = points - 1; index >= 0; index -= 1) {
+ const t = now - index * stepMs;
+ samples.push({
+ t,
+ speedKph: speedAt(blueprint, t),
+ batteryPct: batteryAt(blueprint, t),
+ altitudeM: altitudeAt(blueprint, t),
+ });
+ }
+ return samples;
+}
+
+function pushAlert(
+ alerts: Alert[],
+ startedAt: Map,
+ alert: Omit,
+ now: number,
+): void {
+ const existing = startedAt.get(alert.id);
+ const at = existing ?? now;
+ if (existing === undefined) startedAt.set(alert.id, at);
+ alerts.push({ ...alert, at });
+}
+
+export function deriveAlerts(
+ units: FleetUnit[],
+ startedAt: Map,
+ now: number,
+): Alert[] {
+ const alerts: Alert[] = [];
+ const active = new Set();
+
+ for (const unit of units) {
+ if (unit.link === 'degraded') {
+ const id = `link:${unit.id}`;
+ active.add(id);
+ pushAlert(
+ alerts,
+ startedAt,
+ {
+ id,
+ unitId: unit.id,
+ callsign: unit.callsign,
+ severity: 'warning',
+ code: 'link-degraded',
+ message: `${unit.callsign} telemetry link is intermittent`,
+ },
+ now,
+ );
+ }
+
+ if (unit.status !== 'charging' && unit.batteryPct < 20) {
+ const id = `battery:${unit.id}`;
+ active.add(id);
+ const severity = unit.batteryPct < 12 ? 'critical' : 'warning';
+ pushAlert(
+ alerts,
+ startedAt,
+ {
+ id,
+ unitId: unit.id,
+ callsign: unit.callsign,
+ severity,
+ code: 'low-battery',
+ message: `${unit.callsign} battery at ${unit.batteryPct}%`,
+ },
+ now,
+ );
+ }
+
+ if (
+ unit.kind === 'drone' &&
+ unit.altitudeM > 0 &&
+ inZone(unit.lat, unit.lng, RESTRICTED_ZONE)
+ ) {
+ const id = `zone:${unit.id}`;
+ active.add(id);
+ pushAlert(
+ alerts,
+ startedAt,
+ {
+ id,
+ unitId: unit.id,
+ callsign: unit.callsign,
+ severity: 'critical',
+ code: 'restricted-zone',
+ message: `${unit.callsign} entered ${RESTRICTED_ZONE.name} restricted airspace`,
+ },
+ now,
+ );
+ }
+ }
+
+ for (const id of startedAt.keys()) {
+ if (!active.has(id)) startedAt.delete(id);
+ }
+
+ const severityRank: Record = { critical: 0, warning: 1 };
+ return alerts.sort(
+ (a, b) => severityRank[a.severity] - severityRank[b.severity] || a.at - b.at,
+ );
+}
+
+export interface FleetEngine {
+ snapshot(now?: number): FleetSnapshot;
+ history(unitId: string, now?: number, points?: number): TelemetrySample[];
+ acknowledge(id: string): void;
+}
+
+export function createEngine(): FleetEngine {
+ const startedAt = new Map();
+ const acknowledged = new Set();
+
+ return {
+ snapshot(now = Date.now()) {
+ const units = ROSTER.map((blueprint) => projectUnit(blueprint, now));
+ const alerts = deriveAlerts(units, startedAt, now).filter(
+ (alert) => !acknowledged.has(alert.id),
+ );
+ return { units, alerts, serverTime: now, zone: RESTRICTED_ZONE };
+ },
+ history(unitId, now = Date.now(), points = HISTORY_POINTS) {
+ const blueprint = blueprintById(unitId);
+ if (!blueprint) return [];
+ return historyFor(blueprint, now, points);
+ },
+ acknowledge(id) {
+ acknowledged.add(id);
+ },
+ };
+}
diff --git a/src/sim/geo.test.ts b/src/sim/geo.test.ts
new file mode 100644
index 0000000..0a8fc9d
--- /dev/null
+++ b/src/sim/geo.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest';
+import { RESTRICTED_ZONE } from './roster';
+import { distanceMeters, headingDegrees, inZone } from './geo';
+
+describe('geo', () => {
+ it('measures a zero distance for the same point', () => {
+ expect(distanceMeters({ lat: 37.7, lng: -122.4 }, { lat: 37.7, lng: -122.4 })).toBe(0);
+ });
+
+ it('heads east when longitude increases', () => {
+ const heading = headingDegrees({ lat: 37.7, lng: -122.4 }, { lat: 37.7, lng: -122.3 });
+ expect(heading).toBeGreaterThan(80);
+ expect(heading).toBeLessThan(100);
+ });
+
+ it('treats the zone edges as inside', () => {
+ expect(inZone(RESTRICTED_ZONE.south, RESTRICTED_ZONE.west, RESTRICTED_ZONE)).toBe(true);
+ expect(inZone(RESTRICTED_ZONE.north + 0.01, RESTRICTED_ZONE.east, RESTRICTED_ZONE)).toBe(
+ false,
+ );
+ });
+});
diff --git a/src/sim/geo.ts b/src/sim/geo.ts
new file mode 100644
index 0000000..e926c82
--- /dev/null
+++ b/src/sim/geo.ts
@@ -0,0 +1,70 @@
+import type { LatLng, RestrictedZone } from '../types';
+
+const METERS_PER_DEG_LAT = 111_320;
+
+export function distanceMeters(from: LatLng, to: LatLng): number {
+ const latRad = (from.lat * Math.PI) / 180;
+ const north = (to.lat - from.lat) * METERS_PER_DEG_LAT;
+ const east = (to.lng - from.lng) * METERS_PER_DEG_LAT * Math.cos(latRad);
+ return Math.hypot(east, north);
+}
+
+/** Degrees clockwise from north. */
+export function headingDegrees(from: LatLng, to: LatLng): number {
+ const latRad = (from.lat * Math.PI) / 180;
+ const north = (to.lat - from.lat) * METERS_PER_DEG_LAT;
+ const east = (to.lng - from.lng) * METERS_PER_DEG_LAT * Math.cos(latRad);
+ const degrees = (Math.atan2(east, north) * 180) / Math.PI;
+ return (degrees + 360) % 360;
+}
+
+export interface RouteGeom {
+ points: LatLng[];
+ segmentLengths: number[];
+ total: number;
+}
+
+export function buildRoute(points: LatLng[]): RouteGeom {
+ const segmentLengths: number[] = [];
+ let total = 0;
+ for (let index = 0; index < points.length; index += 1) {
+ const start = points[index];
+ const end = points[(index + 1) % points.length];
+ if (!start || !end) continue;
+ const length = Math.max(distanceMeters(start, end), 1);
+ segmentLengths.push(length);
+ total += length;
+ }
+ return { points, segmentLengths, total: Math.max(total, 1) };
+}
+
+export function pointAlong(
+ route: RouteGeom,
+ distance: number,
+): { lat: number; lng: number; heading: number } {
+ const first = route.points[0];
+ if (!first) {
+ return { lat: 0, lng: 0, heading: 0 };
+ }
+ let remaining = ((distance % route.total) + route.total) % route.total;
+ for (let index = 0; index < route.segmentLengths.length; index += 1) {
+ const length = route.segmentLengths[index] ?? 1;
+ const start = route.points[index];
+ const end = route.points[(index + 1) % route.points.length];
+ if (!start || !end) continue;
+ if (remaining <= length) {
+ const t = remaining / length;
+ return {
+ lat: start.lat + (end.lat - start.lat) * t,
+ lng: start.lng + (end.lng - start.lng) * t,
+ heading: headingDegrees(start, end),
+ };
+ }
+ remaining -= length;
+ }
+ return { lat: first.lat, lng: first.lng, heading: 0 };
+}
+
+export function inZone(lat: number, lng: number, zone: RestrictedZone): boolean {
+ return lat >= zone.south && lat <= zone.north && lng >= zone.west && lng <= zone.east;
+}
diff --git a/src/sim/roster.ts b/src/sim/roster.ts
new file mode 100644
index 0000000..458a57c
--- /dev/null
+++ b/src/sim/roster.ts
@@ -0,0 +1,284 @@
+import type { LatLng, RestrictedZone, UnitBlueprint } from '../types';
+
+const p = (lat: number, lng: number): LatLng => ({ lat, lng });
+
+/** Fictional ops box on the Mission Bay waterfront. Ground traffic may pass under it. */
+export const RESTRICTED_ZONE: RestrictedZone = {
+ name: 'South Basin',
+ south: 37.746,
+ west: -122.398,
+ north: 37.758,
+ east: -122.382,
+};
+
+export const ROSTER: readonly UnitBlueprint[] = [
+ {
+ id: 'm-14',
+ callsign: 'M-14',
+ kind: 'ground',
+ payload: 'Parcels · 18',
+ routeName: 'Embarcadero loop',
+ desk: 'North desk',
+ behavior: 'patrol',
+ cruiseKph: 36,
+ phaseMs: 12_000,
+ batteryPeriodMs: 140_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [
+ p(37.7955, -122.4012),
+ p(37.8068, -122.4095),
+ p(37.8022, -122.4288),
+ p(37.7874, -122.4004),
+ ],
+ },
+ {
+ id: 'm-22',
+ callsign: 'M-22',
+ kind: 'ground',
+ payload: 'Cold chain',
+ routeName: 'Bay Bridge freight',
+ desk: 'North desk',
+ behavior: 'patrol',
+ cruiseKph: 48,
+ phaseMs: 40_000,
+ batteryPeriodMs: 160_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [
+ p(37.7888, -122.392),
+ p(37.8085, -122.364),
+ p(37.8224, -122.326),
+ p(37.8042, -122.338),
+ ],
+ },
+ {
+ id: 'm-07',
+ callsign: 'M-07',
+ kind: 'ground',
+ payload: 'Spare parts',
+ routeName: 'Mission shuttle',
+ desk: 'City desk',
+ behavior: 'patrol',
+ cruiseKph: 32,
+ phaseMs: 8_000,
+ batteryPeriodMs: 130_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [
+ p(37.7598, -122.4194),
+ p(37.7552, -122.441),
+ p(37.7694, -122.4468),
+ p(37.7796, -122.4142),
+ ],
+ },
+ {
+ id: 'm-31',
+ callsign: 'M-31',
+ kind: 'ground',
+ payload: 'Depot transfer',
+ routeName: 'Potrero hauler',
+ desk: 'City desk',
+ behavior: 'patrol',
+ cruiseKph: 34,
+ phaseMs: 55_000,
+ batteryPeriodMs: 150_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [
+ p(37.7604, -122.4055),
+ p(37.7482, -122.416),
+ p(37.7376, -122.4048),
+ p(37.7505, -122.3942),
+ ],
+ },
+ {
+ id: 'm-18',
+ callsign: 'M-18',
+ kind: 'ground',
+ payload: 'Retail cages',
+ routeName: 'Sunset loop',
+ desk: 'West desk',
+ behavior: 'patrol',
+ cruiseKph: 38,
+ phaseMs: 22_000,
+ batteryPeriodMs: 145_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [
+ p(37.7622, -122.4788),
+ p(37.7464, -122.4986),
+ p(37.7328, -122.4812),
+ p(37.751, -122.4634),
+ ],
+ },
+ {
+ id: 'm-41',
+ callsign: 'M-41',
+ kind: 'ground',
+ payload: 'Seafood totes',
+ routeName: 'Wharf spur',
+ desk: 'North desk',
+ behavior: 'patrol',
+ cruiseKph: 28,
+ phaseMs: 70_000,
+ batteryPeriodMs: 125_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [
+ p(37.8084, -122.4196),
+ p(37.8102, -122.4104),
+ p(37.8006, -122.4058),
+ p(37.7988, -122.4172),
+ ],
+ },
+ {
+ id: 'm-03',
+ callsign: 'M-03',
+ kind: 'ground',
+ payload: 'Yard mule',
+ routeName: 'Mission Bay yard',
+ desk: 'Yard',
+ behavior: 'idle',
+ cruiseKph: 0,
+ phaseMs: 0,
+ batteryPeriodMs: 200_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [p(37.7688, -122.3955)],
+ },
+ {
+ id: 'm-09',
+ callsign: 'M-09',
+ kind: 'ground',
+ payload: 'Medical stat',
+ routeName: 'Potrero climb',
+ desk: 'City desk',
+ behavior: 'degraded',
+ cruiseKph: 30,
+ phaseMs: 15_000,
+ batteryPeriodMs: 90_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [
+ p(37.7572, -122.4118),
+ p(37.7614, -122.4036),
+ p(37.7546, -122.3994),
+ p(37.7518, -122.4132),
+ ],
+ },
+ {
+ id: 'd-12',
+ callsign: 'D-12',
+ kind: 'drone',
+ payload: 'Survey lidar',
+ routeName: 'City survey',
+ desk: 'Air wing',
+ behavior: 'patrol',
+ cruiseKph: 54,
+ phaseMs: 5_000,
+ batteryPeriodMs: 120_000,
+ altitudeBase: 90,
+ altitudeAmp: 24,
+ route: [
+ p(37.7852, -122.445),
+ p(37.8004, -122.4302),
+ p(37.7784, -122.4096),
+ p(37.7624, -122.4348),
+ ],
+ },
+ {
+ id: 'd-04',
+ callsign: 'D-04',
+ kind: 'drone',
+ payload: 'Document pouch',
+ routeName: 'Downtown courier',
+ desk: 'Air wing',
+ behavior: 'patrol',
+ cruiseKph: 64,
+ phaseMs: 33_000,
+ batteryPeriodMs: 110_000,
+ altitudeBase: 70,
+ altitudeAmp: 16,
+ route: [
+ p(37.7902, -122.4052),
+ p(37.7934, -122.4184),
+ p(37.7862, -122.4216),
+ p(37.7844, -122.4014),
+ ],
+ },
+ {
+ id: 'd-19',
+ callsign: 'D-19',
+ kind: 'drone',
+ payload: 'Bridge camera',
+ routeName: 'Span inspector',
+ desk: 'Air wing',
+ behavior: 'patrol',
+ cruiseKph: 46,
+ phaseMs: 48_000,
+ batteryPeriodMs: 135_000,
+ altitudeBase: 120,
+ altitudeAmp: 18,
+ route: [
+ p(37.7976, -122.3894),
+ p(37.8118, -122.3696),
+ p(37.8074, -122.3548),
+ p(37.7904, -122.3782),
+ ],
+ },
+ {
+ id: 'd-08',
+ callsign: 'D-08',
+ kind: 'drone',
+ payload: 'Spare airframe',
+ routeName: 'Roof nest',
+ desk: 'Yard',
+ behavior: 'charging',
+ cruiseKph: 0,
+ phaseMs: 20_000,
+ batteryPeriodMs: 180_000,
+ altitudeBase: 0,
+ altitudeAmp: 0,
+ route: [p(37.7702, -122.3915)],
+ },
+ {
+ id: 'd-02',
+ callsign: 'D-02',
+ kind: 'drone',
+ payload: 'Relay radio',
+ routeName: 'SoMa relay',
+ desk: 'Air wing',
+ behavior: 'degraded',
+ cruiseKph: 42,
+ phaseMs: 9_000,
+ batteryPeriodMs: 80_000,
+ altitudeBase: 46,
+ altitudeAmp: 8,
+ route: [
+ p(37.7788, -122.4088),
+ p(37.7826, -122.3996),
+ p(37.7744, -122.3958),
+ p(37.7722, -122.4094),
+ ],
+ },
+ {
+ id: 'd-15',
+ callsign: 'D-15',
+ kind: 'drone',
+ payload: 'Waterfront scan',
+ routeName: 'Basin transit',
+ desk: 'Air wing',
+ behavior: 'patrol',
+ cruiseKph: 58,
+ phaseMs: 2_000,
+ batteryPeriodMs: 150_000,
+ altitudeBase: 80,
+ altitudeAmp: 20,
+ route: [p(37.77, -122.41), p(37.752, -122.39), p(37.736, -122.388), p(37.742, -122.418)],
+ },
+];
+
+export function blueprintById(id: string): UnitBlueprint | undefined {
+ return ROSTER.find((unit) => unit.id === id);
+}
diff --git a/src/store/ui.ts b/src/store/ui.ts
new file mode 100644
index 0000000..32da3ea
--- /dev/null
+++ b/src/store/ui.ts
@@ -0,0 +1,43 @@
+import { create } from 'zustand';
+import type { DataMode, UnitKind, UnitStatus } from '../types';
+
+interface UiState {
+ dataMode: DataMode;
+ linkNote: string;
+ selectedId: string | null;
+ search: string;
+ kind: 'all' | UnitKind;
+ status: 'all' | UnitStatus;
+ acknowledged: Record;
+ setLink: (mode: DataMode, linkNote: string) => void;
+ setSelectedId: (id: string | null) => void;
+ setSearch: (search: string) => void;
+ setKind: (kind: 'all' | UnitKind) => void;
+ setStatus: (status: 'all' | UnitStatus) => void;
+ acknowledge: (id: string) => void;
+ clearFilters: () => void;
+ reset: () => void;
+}
+
+const initial = {
+ dataMode: 'detecting' as DataMode,
+ linkNote: 'Checking for a local simulator.',
+ selectedId: null as string | null,
+ search: '',
+ kind: 'all' as const,
+ status: 'all' as const,
+ acknowledged: {} as Record,
+};
+
+export const useUiStore = create((set) => ({
+ ...initial,
+ setLink: (dataMode, linkNote) => set({ dataMode, linkNote }),
+ setSelectedId: (selectedId) => set({ selectedId }),
+ setSearch: (search) => set({ search }),
+ setKind: (kind) => set({ kind }),
+ setStatus: (status) => set({ status }),
+ acknowledge: (id) =>
+ set((state) => ({ acknowledged: { ...state.acknowledged, [id]: true } })),
+ clearFilters: () => set({ search: '', kind: 'all', status: 'all' }),
+ reset: () => set({ ...initial, acknowledged: {} }),
+}));
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..7fc87a1
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,863 @@
+:root {
+ --canvas-light: oklch(96.4% 0.008 250);
+ --canvas-dark: oklch(17.5% 0.02 262);
+ --surface-light: oklch(99.2% 0.004 250);
+ --surface-dark: oklch(22.4% 0.024 262);
+ --ink-light: oklch(23% 0.028 262);
+ --ink-dark: oklch(95.5% 0.01 250);
+ --muted-light: oklch(46% 0.025 260);
+ --muted-dark: oklch(76% 0.02 255);
+ --line-light: oklch(88% 0.012 255);
+ --line-dark: oklch(33% 0.02 262);
+ --accent-light: oklch(46% 0.09 200);
+ --accent-dark: oklch(82% 0.09 200);
+ --amber-light: oklch(58% 0.13 72);
+ --amber-dark: oklch(82% 0.12 85);
+ --critical-light: oklch(50% 0.16 27);
+ --critical-dark: oklch(76% 0.14 25);
+ --ok-light: oklch(46% 0.1 158);
+ --ok-dark: oklch(80% 0.11 158);
+ --map-bg-light: oklch(92% 0.01 250);
+ --map-bg-dark: oklch(20% 0.02 262);
+ --scrollbar-thumb-light: oklch(72% 0.02 255);
+ --scrollbar-thumb-dark: oklch(42% 0.02 262);
+ --scrollbar-track-light: oklch(94% 0.006 250);
+ --scrollbar-track-dark: oklch(22% 0.02 262);
+
+ --canvas: light-dark(var(--canvas-light), var(--canvas-dark));
+ --surface: light-dark(var(--surface-light), var(--surface-dark));
+ --ink: light-dark(var(--ink-light), var(--ink-dark));
+ --muted: light-dark(var(--muted-light), var(--muted-dark));
+ --line: light-dark(var(--line-light), var(--line-dark));
+ --accent: light-dark(var(--accent-light), var(--accent-dark));
+ --amber: light-dark(var(--amber-light), var(--amber-dark));
+ --critical: light-dark(var(--critical-light), var(--critical-dark));
+ --ok: light-dark(var(--ok-light), var(--ok-dark));
+ --map-bg: light-dark(var(--map-bg-light), var(--map-bg-dark));
+ --scrollbar-thumb: light-dark(var(--scrollbar-thumb-light), var(--scrollbar-thumb-dark));
+ --scrollbar-track: light-dark(var(--scrollbar-track-light), var(--scrollbar-track-dark));
+ --display: 'Fraunces', 'Iowan Old Style', Palatino, serif;
+ --sans: 'Outfit', 'Avenir Next', 'Segoe UI', sans-serif;
+ --mono: 'IBM Plex Mono', ui-monospace, monospace;
+
+ color-scheme: light dark;
+ accent-color: var(--accent);
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+ color: var(--ink);
+ background: var(--canvas);
+ font-family: var(--sans);
+ font-size: 15px;
+ line-height: 1.45;
+}
+
+:root:has(> head > meta[name='color-scheme'][content='dark']) {
+ color-scheme: dark;
+}
+
+:root:has(> head > meta[name='color-scheme'][content='light']) {
+ color-scheme: light;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#root {
+ min-height: 100%;
+}
+
+body {
+ margin: 0;
+ background-color: var(--canvas);
+ background-image: radial-gradient(circle at 1px 1px, var(--line) 1px, transparent 0);
+ background-size: 22px 22px;
+}
+
+button,
+input {
+ font: inherit;
+ color: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+:focus {
+ outline: none;
+}
+
+:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+::selection {
+ background: color-mix(in oklch, var(--accent) 32%, transparent);
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.skip {
+ position: absolute;
+ left: 12px;
+ top: -48px;
+ z-index: 5;
+ background: var(--ink);
+ color: var(--canvas);
+ padding: 8px 12px;
+ border-radius: 8px;
+}
+
+.skip:focus {
+ top: 12px;
+}
+
+.shell {
+ min-height: 100dvh;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+}
+
+.top {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--line);
+ background: var(--surface);
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 220px;
+}
+
+.mark {
+ width: 38px;
+ height: 38px;
+ color: var(--accent);
+}
+
+.brand h1 {
+ margin: 0;
+ font-family: var(--display);
+ font-size: 28px;
+ font-weight: 560;
+ letter-spacing: -0.03em;
+ line-height: 0.95;
+}
+
+.brand p {
+ margin: 4px 0 0;
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.kpis {
+ display: flex;
+ gap: 22px;
+ margin: 0 auto;
+}
+
+.kpis div {
+ min-width: 72px;
+}
+
+.kpis dt {
+ margin: 0;
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.kpis dd {
+ margin: 2px 0 0;
+ font-family: var(--mono);
+ font-size: 18px;
+ font-variant-numeric: tabular-nums;
+}
+
+.top__tools {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.conn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
+ padding: 6px 10px;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.conn__dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--muted);
+}
+
+.conn[data-mode='live'] .conn__dot {
+ background: var(--ok);
+ animation: pulse 1.8s ease-out infinite;
+}
+
+.conn[data-mode='browser'] .conn__dot {
+ background: var(--amber);
+}
+
+@keyframes pulse {
+ 0% {
+ box-shadow: 0 0 0 0 color-mix(in oklch, var(--ok) 55%, transparent);
+ }
+ 100% {
+ box-shadow: 0 0 0 8px transparent;
+ }
+}
+
+.theme {
+ display: flex;
+ padding: 2px;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ background: var(--canvas);
+}
+
+.theme button {
+ border: 0;
+ background: transparent;
+ color: var(--muted);
+ border-radius: 999px;
+ min-height: 32px;
+ padding: 6px 10px;
+ font-size: 12px;
+}
+
+.theme button[aria-pressed='true'] {
+ background: var(--ink);
+ color: var(--canvas);
+}
+
+.workspace {
+ display: grid;
+ grid-template-columns: minmax(280px, 340px) minmax(0, 1fr) minmax(280px, 320px);
+ grid-template-rows: minmax(0, 1.4fr) minmax(240px, 0.85fr);
+ gap: 12px;
+ padding: 12px;
+ min-height: 0;
+}
+
+.fleet {
+ grid-column: 1;
+ grid-row: 1 / span 2;
+}
+
+.map {
+ grid-column: 2;
+ grid-row: 1;
+}
+
+.telemetry {
+ grid-column: 2;
+ grid-row: 2;
+}
+
+.alerts {
+ grid-column: 3;
+ grid-row: 1 / span 2;
+}
+
+.panel {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 16px;
+}
+
+.panel__head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 14px 0;
+}
+
+.panel__head h2 {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.panel__meta {
+ margin: 0;
+ color: var(--muted);
+ font-size: 12px;
+ text-align: right;
+}
+
+.scroll {
+ min-height: 0;
+ overflow: auto;
+}
+
+.filters {
+ display: grid;
+ gap: 8px;
+ padding: 12px 14px 8px;
+}
+
+.search input {
+ width: 100%;
+ border: 1px solid var(--line);
+ background: var(--canvas);
+ border-radius: 10px;
+ padding: 9px 11px;
+}
+
+.chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.chip,
+.text-button {
+ border: 1px solid var(--line);
+ background: transparent;
+ color: var(--muted);
+ border-radius: 999px;
+ min-height: 32px;
+ padding: 4px 10px;
+ font-size: 12px;
+}
+
+.chip[aria-pressed='true'] {
+ background: var(--ink);
+ color: var(--canvas);
+ border-color: var(--ink);
+}
+
+.text-button {
+ justify-self: start;
+ color: var(--accent);
+ border-color: transparent;
+ padding-left: 0;
+}
+
+.hint,
+.empty,
+.loading {
+ margin: 0;
+ padding: 8px 14px 16px;
+ color: var(--muted);
+}
+
+.empty {
+ display: grid;
+ gap: 8px;
+ justify-items: start;
+}
+
+.skeleton {
+ height: 62px;
+ margin: 8px 14px;
+ border-radius: 10px;
+ background: color-mix(in oklch, var(--line) 75%, transparent);
+}
+
+.unit {
+ width: 100%;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+ text-align: left;
+ padding: 11px 14px;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ background: transparent;
+}
+
+.unit:hover {
+ background: color-mix(in oklch, var(--ink) 4%, transparent);
+}
+
+.unit.is-selected {
+ background: color-mix(in oklch, var(--accent) 12%, transparent);
+ box-shadow: inset 3px 0 0 var(--accent);
+}
+
+.unit__icon {
+ display: grid;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 9px;
+ background: var(--canvas);
+ color: var(--accent);
+}
+
+.icon {
+ width: 18px;
+ height: 18px;
+}
+
+.unit__copy {
+ display: grid;
+ gap: 3px;
+ min-width: 0;
+}
+
+.unit__title {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ align-items: baseline;
+}
+
+.unit__call {
+ font-family: var(--mono);
+ font-size: 14px;
+}
+
+.unit__meta {
+ margin: 0;
+ color: var(--muted);
+ font-size: 12px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.unit__stat {
+ display: grid;
+ justify-items: end;
+ font-family: var(--mono);
+ font-variant-numeric: tabular-nums;
+ font-size: 14px;
+}
+
+.unit__stat span:last-child,
+.unit__pct {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.meter-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.meter {
+ flex: 1;
+ height: 6px;
+ border-radius: 999px;
+ background: color-mix(in oklch, var(--line) 80%, transparent);
+ overflow: hidden;
+}
+
+.meter > span {
+ display: block;
+ height: 100%;
+ background: var(--ok);
+}
+
+.meter.is-low > span {
+ background: var(--amber);
+}
+
+.meter.is-critical > span {
+ background: var(--critical);
+}
+
+.status {
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.status::before {
+ content: '';
+ display: inline-block;
+ width: 7px;
+ height: 7px;
+ margin-right: 6px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.status--enroute {
+ color: var(--accent);
+}
+
+.status--charging {
+ color: var(--amber);
+}
+
+.status--alert {
+ color: var(--critical);
+}
+
+.map-frame {
+ position: relative;
+ flex: 1;
+ min-height: 240px;
+ margin: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ border: 1px solid var(--line);
+}
+
+.map-frame .leaflet-container {
+ position: absolute;
+ inset: 0;
+ height: 100%;
+ width: 100%;
+ background: var(--map-bg);
+ font-family: var(--sans);
+}
+
+.leaflet-div-icon.pin-wrap {
+ background: transparent;
+ border: 0;
+}
+
+.pin {
+ display: block;
+ width: 12px;
+ height: 12px;
+ margin: 5px;
+ border-radius: 3px;
+ background: var(--accent);
+ box-shadow: 0 0 0 2px var(--surface);
+}
+
+.pin--drone {
+ border-radius: 50%;
+ background: var(--amber);
+}
+
+.pin--alert {
+ background: var(--critical);
+}
+
+.pin.is-selected {
+ transform: scale(1.45);
+ box-shadow:
+ 0 0 0 2px var(--surface),
+ 0 0 0 4px var(--ink);
+}
+
+.leaflet-control-zoom.leaflet-bar,
+.leaflet-control-zoom.leaflet-bar a {
+ background: var(--surface);
+ color: var(--ink);
+ border-color: var(--line);
+}
+
+.leaflet-control-attribution {
+ background: color-mix(in oklch, var(--surface) 86%, transparent) !important;
+ color: var(--muted);
+ font-size: 10px;
+ max-width: 72%;
+}
+
+.leaflet-control-attribution a {
+ color: var(--accent);
+}
+
+.telemetry__body {
+ display: grid;
+ gap: 10px;
+ padding: 10px 12px 12px;
+}
+
+.telemetry__identity {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ align-items: center;
+}
+
+.telemetry__name {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 0 0 4px;
+ font-family: var(--mono);
+ font-size: 16px;
+}
+
+.dial {
+ width: 46px;
+ height: 46px;
+ flex: none;
+}
+
+.dial circle {
+ fill: none;
+ stroke: var(--line);
+ stroke-width: 2;
+}
+
+.dial path {
+ fill: var(--accent);
+}
+
+.readouts {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ margin: 0;
+}
+
+.readout {
+ padding: 8px 10px;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+}
+
+.readout dt {
+ margin: 0;
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.readout dd {
+ margin: 4px 0 0;
+ font-family: var(--mono);
+ font-size: 18px;
+ font-variant-numeric: tabular-nums;
+}
+
+.readout dd.is-critical {
+ color: var(--critical);
+}
+
+.charts {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.chart {
+ margin: 0;
+ min-width: 0;
+ --series: var(--accent);
+}
+
+.chart--battery {
+ --series: var(--ok);
+}
+
+.chart--altitude {
+ --series: var(--amber);
+}
+
+.chart figcaption {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.chart figcaption strong {
+ color: var(--ink);
+ font-family: var(--mono);
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+}
+
+.chart svg {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+.chart__line {
+ fill: none;
+ stroke: var(--series);
+ stroke-width: 2;
+ stroke-linejoin: round;
+ stroke-linecap: round;
+}
+
+.chart__area {
+ fill: color-mix(in oklch, var(--series) 20%, transparent);
+ stroke: none;
+}
+
+.chart__scale {
+ fill: var(--muted);
+ font-size: 10px;
+ font-family: var(--mono);
+}
+
+.chart__range,
+.chart__empty {
+ margin: 0;
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.alert-list {
+ list-style: none;
+ margin: 0;
+ padding: 4px 0 8px;
+}
+
+.alert {
+ display: grid;
+ gap: 4px;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--line);
+}
+
+.alert__sev {
+ margin: 0;
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.alert--critical .alert__sev {
+ color: var(--critical);
+}
+
+.alert--warning .alert__sev {
+ color: var(--amber);
+}
+
+.alert__msg,
+.alert__meta {
+ margin: 0;
+}
+
+.alert__meta {
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.alert .chip {
+ justify-self: start;
+ margin-top: 4px;
+}
+
+@media (max-width: 1100px) {
+ .shell {
+ height: auto;
+ overflow: visible;
+ }
+
+ .top {
+ flex-wrap: wrap;
+ }
+
+ .kpis,
+ .top__tools {
+ width: 100%;
+ margin: 0;
+ justify-content: space-between;
+ }
+
+ .workspace {
+ display: flex;
+ flex-direction: column;
+ height: auto;
+ }
+
+ .map {
+ order: 1;
+ min-height: 480px;
+ }
+
+ .fleet {
+ order: 2;
+ }
+
+ .telemetry {
+ order: 3;
+ }
+
+ .alerts {
+ order: 4;
+ }
+
+ .fleet .scroll,
+ .alerts .scroll {
+ max-height: 480px;
+ }
+
+ .charts,
+ .readouts {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+@media (max-width: 720px) {
+ .brand h1 {
+ font-size: 24px;
+ }
+
+ .kpis {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px 16px;
+ }
+
+ .charts,
+ .readouts {
+ grid-template-columns: 1fr;
+ }
+
+ .top__tools {
+ flex-wrap: wrap;
+ }
+}
+
+@media (min-width: 1101px) {
+ .shell {
+ height: 100dvh;
+ overflow: hidden;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .conn__dot {
+ animation: none;
+ }
+}
diff --git a/src/test/setup.ts b/src/test/setup.ts
new file mode 100644
index 0000000..a2fa6f3
--- /dev/null
+++ b/src/test/setup.ts
@@ -0,0 +1,24 @@
+import '@testing-library/jest-dom/vitest';
+import { cleanup } from '@testing-library/react';
+import { afterEach } from 'vitest';
+import { useUiStore } from '../store/ui';
+
+if (typeof window !== 'undefined') {
+ window.matchMedia = (query: string) =>
+ ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: () => undefined,
+ removeEventListener: () => undefined,
+ addListener: () => undefined,
+ removeListener: () => undefined,
+ dispatchEvent: () => false,
+ }) as MediaQueryList;
+}
+
+afterEach(() => {
+ if (typeof document !== 'undefined') cleanup();
+ useUiStore.getState().reset();
+ if (typeof localStorage !== 'undefined') localStorage.clear();
+});
diff --git a/src/theme.test.ts b/src/theme.test.ts
new file mode 100644
index 0000000..1a1d86c
--- /dev/null
+++ b/src/theme.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from 'vitest';
+import { applyPreference, metaContentFor, preferenceFromStorage, resolveScheme } from './theme';
+
+describe('color scheme', () => {
+ it('maps the three preferences onto the meta tag', () => {
+ expect(metaContentFor('system')).toBe('light dark');
+ expect(metaContentFor('dark')).toBe('dark');
+ expect(preferenceFromStorage('nope')).toBe('system');
+ expect(resolveScheme('system', 'dark')).toBe('dark');
+ expect(resolveScheme('light', 'dark')).toBe('light');
+ });
+
+ it('pins dark and clears the pin when returning to system', () => {
+ const meta = document.createElement('meta');
+ meta.setAttribute('name', 'color-scheme');
+ meta.setAttribute('content', 'light dark');
+ document.head.append(meta);
+
+ applyPreference('dark');
+ expect(meta.getAttribute('content')).toBe('dark');
+ expect(localStorage.getItem('color-scheme')).toBe('dark');
+
+ applyPreference('system');
+ expect(meta.getAttribute('content')).toBe('light dark');
+ expect(localStorage.getItem('color-scheme')).toBeNull();
+ meta.remove();
+ });
+});
diff --git a/src/theme.ts b/src/theme.ts
new file mode 100644
index 0000000..bdbb4c2
--- /dev/null
+++ b/src/theme.ts
@@ -0,0 +1,61 @@
+export type SchemePreference = 'light' | 'dark' | 'system';
+
+export function preferenceFromStorage(raw: string | null): SchemePreference {
+ if (raw === 'light' || raw === 'dark') return raw;
+ return 'system';
+}
+
+export function metaContentFor(preference: SchemePreference): 'light' | 'dark' | 'light dark' {
+ switch (preference) {
+ case 'light':
+ return 'light';
+ case 'dark':
+ return 'dark';
+ case 'system':
+ return 'light dark';
+ default: {
+ const unreachable: never = preference;
+ return unreachable;
+ }
+ }
+}
+
+export function resolveScheme(
+ preference: SchemePreference,
+ system: 'light' | 'dark',
+): 'light' | 'dark' {
+ switch (preference) {
+ case 'system':
+ return system;
+ case 'light':
+ case 'dark':
+ return preference;
+ default: {
+ const unreachable: never = preference;
+ return unreachable;
+ }
+ }
+}
+
+export function readMetaContent(): string {
+ return (
+ document.querySelector('meta[name="color-scheme"]')?.getAttribute('content') ?? 'light dark'
+ );
+}
+
+export function preferenceFromMeta(content: string): SchemePreference {
+ if (content === 'light' || content === 'dark') return content;
+ return 'system';
+}
+
+/** Persist a deliberate light, dark, or system choice and reflect it on the root meta tag. */
+export function applyPreference(preference: SchemePreference): void {
+ const content = metaContentFor(preference);
+ document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', content);
+ try {
+ if (preference === 'system') localStorage.removeItem('color-scheme');
+ else localStorage.setItem('color-scheme', preference);
+ } catch {
+ // Storage can throw when site data is blocked. The meta tag still updates this tab.
+ }
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..f2a8935
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,82 @@
+export type UnitKind = 'ground' | 'drone';
+
+export type UnitStatus = 'enroute' | 'idle' | 'charging' | 'alert';
+
+export type Severity = 'warning' | 'critical';
+
+export type AlertCode = 'low-battery' | 'restricted-zone' | 'link-degraded';
+
+export type UnitBehavior = 'patrol' | 'idle' | 'charging' | 'degraded';
+
+export type DataMode = 'detecting' | 'live' | 'browser';
+
+export interface LatLng {
+ lat: number;
+ lng: number;
+}
+
+export interface FleetUnit {
+ id: string;
+ callsign: string;
+ kind: UnitKind;
+ status: UnitStatus;
+ lat: number;
+ lng: number;
+ heading: number;
+ speedKph: number;
+ batteryPct: number;
+ altitudeM: number;
+ payload: string;
+ routeName: string;
+ desk: string;
+ link: 'nominal' | 'degraded';
+ updatedAt: number;
+}
+
+export interface Alert {
+ id: string;
+ unitId: string;
+ callsign: string;
+ severity: Severity;
+ code: AlertCode;
+ message: string;
+ at: number;
+}
+
+export interface RestrictedZone {
+ name: string;
+ south: number;
+ west: number;
+ north: number;
+ east: number;
+}
+
+export interface TelemetrySample {
+ t: number;
+ speedKph: number;
+ batteryPct: number;
+ altitudeM: number;
+}
+
+export interface FleetSnapshot {
+ units: FleetUnit[];
+ alerts: Alert[];
+ serverTime: number;
+ zone: RestrictedZone;
+}
+
+export interface UnitBlueprint {
+ id: string;
+ callsign: string;
+ kind: UnitKind;
+ payload: string;
+ routeName: string;
+ desk: string;
+ behavior: UnitBehavior;
+ cruiseKph: number;
+ phaseMs: number;
+ batteryPeriodMs: number;
+ altitudeBase: number;
+ altitudeAmp: number;
+ route: LatLng[];
+}
diff --git a/src/useSystemScheme.ts b/src/useSystemScheme.ts
new file mode 100644
index 0000000..41c1a81
--- /dev/null
+++ b/src/useSystemScheme.ts
@@ -0,0 +1,18 @@
+import { useEffect, useState } from 'react';
+
+function systemScheme(): 'light' | 'dark' {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+}
+
+export function useSystemScheme(): 'light' | 'dark' {
+ const [scheme, setScheme] = useState<'light' | 'dark'>(systemScheme);
+
+ useEffect(() => {
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
+ const onChange = () => setScheme(media.matches ? 'dark' : 'light');
+ media.addEventListener('change', onChange);
+ return () => media.removeEventListener('change', onChange);
+ }, []);
+
+ return scheme;
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..521dd41
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_FORCE_BROWSER?: string;
+ readonly VITE_WS_URL?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..153efe8
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..f6b4bff
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "forceConsistentCasingInFileNames": true,
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts", "playwright.config.ts", "server", "e2e", "scripts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..30d98ea
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,20 @@
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ // Relative assets so the same build works on GitHub project pages and `vite preview`.
+ base: './',
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': { target: 'http://127.0.0.1:8787', changeOrigin: true },
+ '/ws': { target: 'ws://127.0.0.1:8787', ws: true },
+ },
+ },
+ test: {
+ environment: 'jsdom',
+ setupFiles: './src/test/setup.ts',
+ css: true,
+ exclude: ['e2e/**', 'node_modules/**', 'dist/**'],
+ },
+});