diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
new file mode 100644
index 0000000..22280dc
--- /dev/null
+++ b/.github/workflows/frontend.yml
@@ -0,0 +1,39 @@
+name: Frontend CI
+
+on:
+ push:
+ paths:
+ - 'frontend/**'
+ - '.github/workflows/frontend.yml'
+ pull_request:
+ paths:
+ - 'frontend/**'
+ - '.github/workflows/frontend.yml'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: frontend
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ cache-dependency-path: frontend/package-lock.json
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Test
+ run: npm run test
+
+ - name: Build
+ run: npm run build
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json
new file mode 100644
index 0000000..6fa991d
--- /dev/null
+++ b/frontend/.oxlintrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 0000000..0319992
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "printWidth": 100,
+ "tabWidth": 2,
+ "arrowParens": "always"
+}
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..691b82e
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,80 @@
+# spanLedger Frontend
+
+Dark, calm, surgical UI for the spanLedger reliability pipeline auditor.
+
+## Prerequisites
+
+- Node.js ≥ 18
+- Python ≥ 3.11 (to run the backend)
+
+## Development
+
+### 1. Start the backend
+
+```sh
+# From the repo root
+python -m spanledger run --config demo/spanledger.demo.yaml
+```
+
+The backend serves the API at `http://localhost:8231`.
+
+### 2. Start the frontend dev server
+
+```sh
+# From this directory (frontend/)
+npm install
+npm run dev
+```
+
+The dev server runs at `http://localhost:5173` and proxies all `/api`, `/status`, `/findings`, and `/healthz` requests to the backend — no CORS configuration required.
+
+## Available Scripts
+
+| Command | Description |
+|---------|-------------|
+| `npm run dev` | Start Vite dev server with HMR |
+| `npm run build` | TypeScript check + production bundle |
+| `npm run preview` | Serve the production build locally |
+| `npm run test` | Run all unit tests (vitest) |
+| `npm run lint` | ESLint + Prettier check |
+| `npm run lint:fix` | Auto-fix lint and format issues |
+
+## Simulated Data Mode
+
+To run without a backend (e.g., for demo rehearsal):
+
+```sh
+npm run dev -- -- --mode mock
+# or navigate to: http://localhost:5173/?data=sim
+```
+
+A **SIMULATED DATA** badge will appear in the top bar. The UI never presents simulated data without this label.
+
+## Architecture
+
+See [`docs/frontend/FRONTEND_ARCHITECTURE.md`](../docs/frontend/FRONTEND_ARCHITECTURE.md) for the full design.
+
+Key points:
+- **Framework**: Vite 5 + React 18 + TypeScript (strict)
+- **Styling**: Tailwind CSS v3 + CSS custom properties (design tokens)
+- **Server state**: TanStack Query v5
+- **Routing**: react-router-dom v6
+- **Charts**: Recharts
+- **Icons**: lucide-react
+
+## Folder Structure
+
+```
+src/
+ api/ API client, types, normalizer, query hooks, mock client
+ components/
+ layout/ RootLayout, Sidebar, TopBar, ConnectionBanner, PageHeader
+ ui/ Card, Badge, Button, Select, Table, Skeleton, Toast, ...
+ slo/ BudgetGauge, BurnRateBars, SliStat, ...
+ events/ EventRow, EventClassBadge, GapRunChart, ...
+ charts/ SliHistoryChart, TimelineAxis, ...
+ lib/ Pure utilities: time, format, slo, signoz-links, settings
+ pages/ One directory per route, page-local components
+ providers/ UiProvider, DemoProvider
+ theme/ tokens.css — the single source of truth for all design tokens
+```
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..bebb5c5
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,75 @@
+import js from '@eslint/js'
+import tsParser from '@typescript-eslint/parser'
+import tsPlugin from '@typescript-eslint/eslint-plugin'
+import reactPlugin from 'eslint-plugin-react'
+import reactHooksPlugin from 'eslint-plugin-react-hooks'
+
+export default [
+ js.configs.recommended,
+ {
+ files: ['src/**/*.{ts,tsx}'],
+ languageOptions: {
+ parser: tsParser,
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ ecmaFeatures: { jsx: true },
+ },
+ globals: {
+ window: 'readonly',
+ document: 'readonly',
+ navigator: 'readonly',
+ localStorage: 'readonly',
+ fetch: 'readonly',
+ URL: 'readonly',
+ AbortController: 'readonly',
+ setTimeout: 'readonly',
+ clearTimeout: 'readonly',
+ setInterval: 'readonly',
+ clearInterval: 'readonly',
+ Response: 'readonly',
+ MouseEvent: 'readonly',
+ KeyboardEvent: 'readonly',
+ Node: 'readonly',
+ HTMLDivElement: 'readonly',
+ HTMLButtonElement: 'readonly',
+ HTMLInputElement: 'readonly',
+ HTMLTextAreaElement: 'readonly',
+ SVGPathElement: 'readonly',
+ MediaQueryListEvent: 'readonly',
+ URLSearchParams: 'readonly',
+ React: 'readonly',
+ },
+ },
+ plugins: {
+ '@typescript-eslint': tsPlugin,
+ react: reactPlugin,
+ 'react-hooks': reactHooksPlugin,
+ },
+ rules: {
+ ...tsPlugin.configs.recommended.rules,
+ ...reactPlugin.configs.recommended.rules,
+ ...reactHooksPlugin.configs.recommended.rules,
+ // No hex colors in src/ outside theme/ (design system rule)
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector: 'Literal[value=/#[0-9a-fA-F]{3,8}/]',
+ message:
+ 'No hardcoded hex colors in src/ outside theme/tokens.css. Use CSS variables.',
+ },
+ ],
+ 'react/react-in-jsx-scope': 'off',
+ 'react/prop-types': 'off',
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/no-explicit-any': 'warn',
+ },
+ settings: {
+ react: { version: 'detect' },
+ },
+ },
+ {
+ ignores: ['dist/', 'node_modules/', '*.config.js', '*.config.ts'],
+ },
+]
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..a1ed28d
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ spanLedger — Reliability Pipeline Auditor
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..fc88687
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,7865 @@
+{
+ "name": "spanledger-frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "spanledger-frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tanstack/react-query": "^5.83.0",
+ "lucide-react": "^0.474.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.6.3",
+ "recharts": "^2.15.3"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^22.15.3",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@typescript-eslint/eslint-plugin": "^8.36.0",
+ "@typescript-eslint/parser": "^8.36.0",
+ "@vitejs/plugin-react": "^4.5.2",
+ "autoprefixer": "^10.4.21",
+ "eslint": "^9.26.0",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "jsdom": "^26.1.0",
+ "postcss": "^8.5.3",
+ "prettier": "^3.5.3",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5",
+ "vitest": "^3.2.4"
+ }
+ },
+ "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/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@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-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "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.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "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/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
+ "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.3.0",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz",
+ "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.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.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
+ "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
+ "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.101.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "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": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "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": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "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/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.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/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
+ "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
+ "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/type-utils": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "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.65.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
+ "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "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.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
+ "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.65.0",
+ "@typescript-eslint/types": "^8.65.0",
+ "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.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
+ "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
+ "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
+ "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.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
+ "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0",
+ "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.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
+ "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
+ "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.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
+ "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.65.0",
+ "@typescript-eslint/tsconfig-utils": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
+ "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.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/visitor-keys": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
+ "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.7",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.7",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "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": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/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/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.4",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz",
+ "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.6",
+ "caniuse-lite": "^1.0.30001806",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/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.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz",
+ "integrity": "sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+ "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.7",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
+ "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+ "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.10.44",
+ "caniuse-lite": "^1.0.30001806",
+ "electron-to-chromium": "^1.5.393",
+ "node-releases": "^2.0.51",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001806",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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.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/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "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/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.395",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz",
+ "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-abstract-get": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz",
+ "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.2",
+ "is-callable": "^1.2.7",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz",
+ "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.1.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.3.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.5",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz",
+ "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-abstract-get": "^1.0.0",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.1.0",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
+ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.6",
+ "@eslint/js": "9.39.5",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint-plugin-react/node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "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/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/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/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "license": "MIT"
+ },
+ "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-equals": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz",
+ "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
+ "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2",
+ "hasown": "^2.0.4",
+ "is-callable": "^1.2.7",
+ "is-document.all": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-document.all": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz",
+ "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "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/js-yaml": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "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/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.474.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz",
+ "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "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": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/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.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "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/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "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-exports-info": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz",
+ "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array.prototype.flatmap": "^1.3.3",
+ "es-errors": "^1.3.0",
+ "object.entries": "^1.1.9",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/node-exports-info/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.51",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/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/own-keys": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz",
+ "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.21",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz",
+ "integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==",
+ "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.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-import/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
+ "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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.6",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
+ "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/pretty-format/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/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "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/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.8"
+ }
+ },
+ "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-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.18.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
+ "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.18.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
+ "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.18.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-smooth": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-equals": "^5.0.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/recharts": {
+ "version": "2.15.4",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
+ "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
+ "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "eventemitter3": "^4.0.1",
+ "lodash": "^4.17.21",
+ "react-is": "^18.3.1",
+ "react-smooth": "^4.0.4",
+ "recharts-scale": "^0.4.4",
+ "tiny-invariant": "^1.3.1",
+ "victory-vendor": "^36.6.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/recharts-scale": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
+ "license": "MIT",
+ "dependencies": {
+ "decimal.js-light": "^2.4.1"
+ }
+ },
+ "node_modules/recharts/node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "license": "MIT"
+ },
+ "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/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "2.0.0-next.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
+ "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.2",
+ "node-exports-info": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
+ "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "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/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "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/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz",
+ "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-object-atoms": "^1.1.2",
+ "has-property-descriptors": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz",
+ "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.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/tinyglobby/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/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz",
+ "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "for-each": "^0.3.5",
+ "gopd": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "possible-typed-array-names": "^1.1.0",
+ "reflect.getprototypeof": "^1.0.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/victory-vendor": {
+ "version": "36.9.2",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite/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/vite/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.7",
+ "@vitest/mocker": "3.2.7",
+ "@vitest/pretty-format": "^3.2.7",
+ "@vitest/runner": "3.2.7",
+ "@vitest/snapshot": "3.2.7",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.7",
+ "@vitest/ui": "3.2.7",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "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": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.22",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
+ "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
+ "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/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/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"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..cbebb05
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "spanledger-frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "lint": "eslint src --ext .ts,.tsx --max-warnings 0 && prettier --check \"src/**/*.{ts,tsx,css}\"",
+ "lint:fix": "eslint src --ext .ts,.tsx --fix && prettier --write \"src/**/*.{ts,tsx,css}\""
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.83.0",
+ "lucide-react": "^0.474.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.6.3",
+ "recharts": "^2.15.3"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^22.15.3",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@typescript-eslint/eslint-plugin": "^8.36.0",
+ "@typescript-eslint/parser": "^8.36.0",
+ "@vitejs/plugin-react": "^4.5.2",
+ "autoprefixer": "^10.4.21",
+ "eslint": "^9.26.0",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "jsdom": "^26.1.0",
+ "postcss": "^8.5.3",
+ "prettier": "^3.5.3",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5",
+ "vitest": "^3.2.4"
+ }
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..aa70493
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1 @@
+/* App.css cleared — all styles managed via tokens.css and index.css */
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..ff79d6c
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,31 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { RouterProvider } from 'react-router-dom'
+import { UiProvider } from '@/providers/UiProvider'
+import { ToastProvider } from '@/components/ui/Toast'
+import { router } from '@/router'
+
+const isDemoMode =
+ typeof window !== 'undefined' &&
+ (window.location.search.includes('data=sim') || window.location.pathname.startsWith('/demo'))
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: true,
+ retry: 1,
+ refetchIntervalInBackground: isDemoMode,
+ },
+ },
+})
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
new file mode 100644
index 0000000..2b018ac
--- /dev/null
+++ b/frontend/src/api/client.ts
@@ -0,0 +1,215 @@
+/**
+ * API client — the only module allowed to make HTTP calls to the backend.
+ *
+ * Rules (from FRONTEND_ARCHITECTURE.md):
+ * - Never call :8231 cross-origin — always use same-origin via the Vite proxy.
+ * - 5s timeout via AbortController.
+ * - Errors parsed as RFC 7807 problem+json where available.
+ * - Unknown query params → 400 from the backend, surfaced as ApiError.
+ * - Never append cache busters or analytics params.
+ */
+
+import type { ProblemJson, DeployMarkerRequest } from './types'
+import { getSettings } from '@/lib/settings'
+import { mockHealthz, mockStatus, mockSlo, mockSloHistory, mockEvents } from './mock/mockClient'
+
+/** Checked-in helper to detect simulation mode */
+export function isSimulatedMode(): boolean {
+ if (typeof window === 'undefined') return false
+ const params = new URLSearchParams(window.location.search)
+ return (
+ params.get('data') === 'sim' ||
+ import.meta.env.MODE === 'mock' ||
+ Boolean(import.meta.env.VITE_MOCK)
+ )
+}
+
+/** Typed error from the API layer — wraps RFC 7807 or network failures */
+export class ApiError extends Error {
+ readonly status: number
+ readonly title: string
+ readonly detail: string | undefined
+
+ constructor(status: number, title: string, detail?: string) {
+ super(title)
+ this.name = 'ApiError'
+ this.status = status
+ this.title = title
+ this.detail = detail
+ }
+}
+
+/** Generic GET against the spanledger backend.
+ * `params` values are coerced to strings and appended as query parameters.
+ * Pass only params explicitly documented in FRONTEND_ARCHITECTURE.md — the
+ * backend rejects unknown query params with a 400. */
+export async function apiGet(
+ path: string,
+ params?: Record
+): Promise {
+ if (isSimulatedMode()) {
+ if (path === '/healthz') {
+ return Promise.resolve(mockHealthz() as unknown as T)
+ }
+ if (path === '/api/v2/status') {
+ return Promise.resolve(mockStatus() as unknown as T)
+ }
+ if (path === '/api/v2/slo') {
+ return Promise.resolve(mockSlo() as unknown as T)
+ }
+ if (path.startsWith('/api/v2/slo/') && path.endsWith('/history')) {
+ return Promise.resolve(mockSloHistory() as unknown as T)
+ }
+ if (path.startsWith('/api/v2/streams/')) {
+ const parts = path.split('/')
+ const name = decodeURIComponent(parts[parts.length - 1] || '')
+ const snap = mockStatus().streams[name]
+ if (snap) {
+ return Promise.resolve(snap as unknown as T)
+ }
+ throw new ApiError(404, 'Stream not found')
+ }
+ if (path === '/api/v2/events') {
+ return Promise.resolve(mockEvents() as unknown as T)
+ }
+ if (path.startsWith('/api/v2/events/')) {
+ const parts = path.split('/')
+ const id = decodeURIComponent(parts[parts.length - 1] || '')
+ const evs = mockEvents().events
+ const found = evs.find((e) => e.id === id)
+ if (found) {
+ return Promise.resolve(found as unknown as T)
+ }
+ throw new ApiError(404, 'Event not found')
+ }
+ throw new ApiError(404, `Mock path not implemented: ${path}`)
+ }
+
+ const settings = getSettings()
+ const baseUrl = settings.apiBaseUrl // default '' = same-origin proxy
+
+ const url = new URL(path, baseUrl ? baseUrl : window.location.origin)
+ if (params) {
+ for (const [key, value] of Object.entries(params)) {
+ if (value !== null && value !== undefined) {
+ url.searchParams.set(key, String(value))
+ }
+ }
+ }
+
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), 5_000)
+
+ let response: Response
+ try {
+ response = await fetch(url.toString(), {
+ signal: controller.signal,
+ headers: {
+ Accept: 'application/json',
+ },
+ })
+ } catch (err) {
+ clearTimeout(timeoutId)
+ if (err instanceof Error && err.name === 'AbortError') {
+ throw new ApiError(0, 'Request timed out', `GET ${path} exceeded 5s`)
+ }
+ throw new ApiError(0, 'Network error', err instanceof Error ? err.message : String(err))
+ }
+ clearTimeout(timeoutId)
+
+ if (!response.ok) {
+ const contentType = response.headers.get('Content-Type') ?? ''
+ if (contentType.includes('problem+json') || contentType.includes('application/json')) {
+ let problem: Partial
+ try {
+ problem = (await response.json()) as Partial
+ } catch {
+ throw new ApiError(response.status, response.statusText)
+ }
+ throw new ApiError(
+ problem.status ?? response.status,
+ problem.title ?? response.statusText,
+ problem.detail
+ )
+ }
+ throw new ApiError(response.status, response.statusText)
+ }
+
+ return response.json() as Promise
+}
+
+/** POST with JSON body. Optionally supply an Idempotency-Key header. */
+export async function apiPost(
+ path: string,
+ body: TBody,
+ options?: { idempotencyKey?: string }
+): Promise {
+ if (isSimulatedMode()) {
+ if (path === '/api/v2/deploy-markers') {
+ const marker = body as unknown as DeployMarkerRequest
+ return Promise.resolve({
+ id: `sim-deploy-${Date.now()}`,
+ at: marker.at || new Date().toISOString(),
+ scope: marker.scope,
+ stream: marker.stream || null,
+ label: marker.label,
+ config_hash: marker.config_hash || 'sim-hash-12345',
+ source: 'simulated',
+ } as unknown as TResponse)
+ }
+ throw new ApiError(404, `Mock POST path not implemented: ${path}`)
+ }
+
+ const settings = getSettings()
+ const baseUrl = settings.apiBaseUrl
+
+ const url = new URL(path, baseUrl ? baseUrl : window.location.origin)
+
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), 5_000)
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ }
+ if (options?.idempotencyKey) {
+ headers['Idempotency-Key'] = options.idempotencyKey
+ }
+
+ let response: Response
+ try {
+ response = await fetch(url.toString(), {
+ method: 'POST',
+ signal: controller.signal,
+ headers,
+ body: JSON.stringify(body),
+ })
+ } catch (err) {
+ clearTimeout(timeoutId)
+ if (err instanceof Error && err.name === 'AbortError') {
+ throw new ApiError(0, 'Request timed out', `POST ${path} exceeded 5s`)
+ }
+ throw new ApiError(0, 'Network error', err instanceof Error ? err.message : String(err))
+ }
+ clearTimeout(timeoutId)
+
+ if (!response.ok) {
+ const contentType = response.headers.get('Content-Type') ?? ''
+ if (contentType.includes('problem+json') || contentType.includes('application/json')) {
+ let problem: Partial
+ try {
+ problem = (await response.json()) as Partial
+ } catch {
+ throw new ApiError(response.status, response.statusText)
+ }
+ throw new ApiError(
+ problem.status ?? response.status,
+ problem.title ?? response.statusText,
+ problem.detail
+ )
+ }
+ throw new ApiError(response.status, response.statusText)
+ }
+
+ return response.json() as Promise
+}
diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts
new file mode 100644
index 0000000..10bffc6
--- /dev/null
+++ b/frontend/src/api/hooks.ts
@@ -0,0 +1,214 @@
+/**
+ * Query hooks — one per endpoint. Components import hooks, never client.ts.
+ *
+ * Poll intervals derived from backend cadence (FRONTEND_ARCHITECTURE.md):
+ * status: 5s refetch / 4s stale
+ * slo: 10s / 8s
+ * events: 10s / 8s (first page only — deeper pages never auto-refetch)
+ * sloHistory: 60s / 55s
+ * event(id): 15s while incident open, else off
+ * healthz: 5s
+ *
+ * Never call client.ts or fetch directly from components — all data access goes
+ * through these hooks.
+ */
+
+import {
+ useQuery,
+ useInfiniteQuery,
+ useMutation,
+ useQueryClient,
+ type InfiniteData,
+} from '@tanstack/react-query'
+import { apiGet, apiPost } from './client'
+import { normalizeEvents, normalizeEvent } from './normalize'
+import { getSettings } from '@/lib/settings'
+import type {
+ HealthzResponse,
+ V2StatusResponse,
+ V2SloResponse,
+ V2SloHistoryResponse,
+ V2EventsResponse,
+ V2EventResponse,
+ DeployMarkerRequest,
+ DeployMarkerResponse,
+ SignalType,
+ StreamSnapshot,
+} from './types'
+import type { SpanledgerEvent } from './normalize'
+
+/** Dynamic poll interval calculation reading pollIntervalMultiplier setting */
+function getPollInterval(baseMs: number): number {
+ const mult = getSettings().pollIntervalMultiplier
+ return Math.round(baseMs * (mult > 0 ? mult : 1))
+}
+
+// ─── GET /healthz ─────────────────────────────────────────────────────────
+
+export function useHealthz() {
+ return useQuery({
+ queryKey: ['healthz'],
+ queryFn: () => apiGet('/healthz'),
+ refetchInterval: () => getPollInterval(5_000),
+ retry: 1,
+ })
+}
+
+// ─── GET /api/v2/status ───────────────────────────────────────────────────
+
+export function useStatus(signal: SignalType = 'traces') {
+ return useQuery({
+ queryKey: ['status', signal],
+ queryFn: () => apiGet('/api/v2/status', { signal }),
+ refetchInterval: () => getPollInterval(5_000),
+ staleTime: 4_000,
+ retry: 1,
+ })
+}
+
+// ─── GET /api/v2/streams/{name} ───────────────────────────────────────────
+
+export function useStream(name: string, signal: SignalType = 'traces') {
+ return useQuery({
+ queryKey: ['stream', name, signal],
+ queryFn: () =>
+ apiGet(`/api/v2/streams/${encodeURIComponent(name)}`, { signal }),
+ refetchInterval: () => getPollInterval(5_000),
+ staleTime: 4_000,
+ retry: 1,
+ enabled: Boolean(name),
+ })
+}
+
+// ─── GET /api/v2/slo ──────────────────────────────────────────────────────
+
+export function useSlo(signal: SignalType = 'traces') {
+ return useQuery({
+ queryKey: ['slo', signal],
+ queryFn: () => apiGet('/api/v2/slo', { signal }),
+ refetchInterval: () => getPollInterval(10_000),
+ staleTime: 8_000,
+ retry: 1,
+ })
+}
+
+// ─── GET /api/v2/slo/{stream}/history ─────────────────────────────────────
+
+export function useSloHistory(
+ stream: string,
+ signal: SignalType = 'traces',
+ resolution: '1h' | '1d' = '1h'
+) {
+ return useQuery({
+ queryKey: ['sloHistory', stream, signal, resolution],
+ queryFn: () =>
+ apiGet(`/api/v2/slo/${encodeURIComponent(stream)}/history`, {
+ resolution,
+ signal,
+ }),
+ refetchInterval: () => getPollInterval(60_000),
+ staleTime: 55_000,
+ retry: 1,
+ enabled: Boolean(stream),
+ })
+}
+
+// ─── GET /api/v2/events (infinite) ───────────────────────────────────────
+
+interface EventFilters {
+ stream?: string
+ class?: string
+ severity?: string
+ from?: number // nanoseconds
+ to?: number // nanoseconds
+ limit?: number
+}
+
+interface EventPage {
+ events: SpanledgerEvent[]
+ next_cursor: string | null
+}
+
+export function useEvents(filters: EventFilters = {}) {
+ return useInfiniteQuery<
+ EventPage,
+ Error,
+ InfiniteData,
+ [string, EventFilters],
+ string | null
+ >({
+ queryKey: ['events', filters],
+ queryFn: async ({ pageParam }) => {
+ const params: Record = {
+ ...filters,
+ limit: filters.limit ?? 100,
+ }
+ if (pageParam) {
+ params['cursor'] = pageParam
+ }
+ const raw = await apiGet('/api/v2/events', params)
+ return {
+ events: normalizeEvents(raw.events as unknown[]),
+ next_cursor: raw.next_cursor,
+ }
+ },
+ initialPageParam: null,
+ getNextPageParam: (lastPage) => lastPage.next_cursor ?? null,
+ refetchInterval: () => getPollInterval(10_000),
+ staleTime: 8_000,
+ retry: 1,
+ maxPages: 5, // cap at 5 pages per PENDING_FRONTEND.md (timeline virtualization deferred)
+ })
+}
+
+// ─── GET /api/v2/events/{id} ──────────────────────────────────────────────
+
+/** Poll while incident is open (recovery not seen); see FRONTEND_ARCHITECTURE.md
+ * caching table. Open loss events are extended in place (same id) — details page
+ * must track growth. */
+export function useEvent(id: string, isOpen = true) {
+ return useQuery({
+ queryKey: ['event', id],
+ queryFn: async () => {
+ const raw = await apiGet(`/api/v2/events/${encodeURIComponent(id)}`)
+ return normalizeEvent(raw)
+ },
+ refetchInterval: isOpen ? () => getPollInterval(15_000) : false,
+ retry: 1,
+ enabled: Boolean(id),
+ })
+}
+
+// ─── POST /api/v2/deploy-markers ─────────────────────────────────────────
+
+/** Always sends an Idempotency-Key (generated when the form opens) so a
+ * double-click or retry can never double-post — the backend upserts by key. */
+export function usePostDeployMarker() {
+ const queryClient = useQueryClient()
+ return useMutation<
+ DeployMarkerResponse,
+ Error,
+ { body: DeployMarkerRequest; idempotencyKey: string }
+ >({
+ mutationFn: ({ body, idempotencyKey }) =>
+ apiPost('/api/v2/deploy-markers', body, {
+ idempotencyKey,
+ }),
+ onSuccess: () => {
+ // Invalidate events query so the deploy marker appears in the timeline
+ void queryClient.invalidateQueries({ queryKey: ['events'] })
+ },
+ })
+}
+
+// ─── GET /api/v2/ledger/flow ──────────────────────────────────────────────
+
+export function useLedgerFlow(enabled = true) {
+ return useQuery({
+ queryKey: ['ledgerFlow'],
+ queryFn: () => apiGet('/api/v2/ledger/flow'),
+ retry: false,
+ enabled,
+ })
+}
+
diff --git a/frontend/src/api/mock/mockClient.ts b/frontend/src/api/mock/mockClient.ts
new file mode 100644
index 0000000..ec71b6a
--- /dev/null
+++ b/frontend/src/api/mock/mockClient.ts
@@ -0,0 +1,205 @@
+/**
+ * Mock API client — implements the same interface as the real API client.
+ * Replays a recorded fixture timeline (healthy → loss → recovery loop).
+ *
+ * Activation: `?data=sim` URL param or `VITE_MOCK=1` environment variable.
+ *
+ * Rule (FRONTEND_ARCHITECTURE.md F9): zero component code branches on this —
+ * the mock lives entirely behind the API interface.
+ *
+ * The TopBar shows a permanent "SIMULATED DATA" badge when this is active.
+ * This product's brand is honesty: the UI never passes simulated data as live.
+ */
+
+import type {
+ HealthzResponse,
+ V2StatusResponse,
+ V2SloResponse,
+ V2SloHistoryResponse,
+ V2EventsResponse,
+ RawEvent,
+} from '@/api/types'
+
+// ─── Fixture data (representative of a real demo session) ──────────────────
+
+const EPOCH = 'sim-epoch-01hw3x0000000000000000'
+const STREAM_A = 'gateway-a'
+const STREAM_B = 'gateway-b'
+
+/** Simulation phase: 0=healthy, 1=loss growing, 2=loss peak, 3=recovery */
+type SimPhase = 0 | 1 | 2 | 3
+
+const PHASE_DURATION_MS = 45_000 // 45s per phase; full loop = 3 minutes
+
+function getSimPhase(): SimPhase {
+ const loopMs = 4 * PHASE_DURATION_MS
+ const elapsed = Date.now() % loopMs
+ return Math.floor(elapsed / PHASE_DURATION_MS) as SimPhase
+}
+
+function getSliForPhase(phase: SimPhase): number | null {
+ switch (phase) {
+ case 0:
+ return 0.9997
+ case 1:
+ return 0.94
+ case 2:
+ return 0.88
+ case 3:
+ return 0.9998
+ }
+}
+
+function getBudgetForPhase(phase: SimPhase): number {
+ switch (phase) {
+ case 0:
+ return 0.82
+ case 1:
+ return 0.45
+ case 2:
+ return 0.12
+ case 3:
+ return 0.8
+ }
+}
+
+function getBurnRateForPhase(phase: SimPhase) {
+ switch (phase) {
+ case 0:
+ return { '5m': 0.1, '1h': 0.2, '6h': 0.3, '3d': 0.4 }
+ case 1:
+ return { '5m': 18.0, '1h': 15.2, '6h': 8.0, '3d': 3.0 }
+ case 2:
+ return { '5m': 22.0, '1h': 18.0, '6h': 12.0, '3d': 5.0 }
+ case 3:
+ return { '5m': 0.2, '1h': 0.5, '6h': 1.2, '3d': 1.8 }
+ }
+}
+
+// ─── Mock API responses ───────────────────────────────────────────────────
+
+export function mockHealthz(): HealthzResponse {
+ return { status: 'ok' }
+}
+
+export function mockStatus(): V2StatusResponse {
+ const phase = getSimPhase()
+ const sli = getSliForPhase(phase)
+ const budget = getBudgetForPhase(phase)
+ const burns = getBurnRateForPhase(phase)
+
+ const makeStream = (name: string) => ({
+ stream: name,
+ signal: 'traces' as const,
+ sent: 3600,
+ verified: Math.floor((sli ?? 0.99) * 3600),
+ missing: Math.floor((1 - (sli ?? 0.99)) * 3600),
+ duplicate: 2,
+ delivery_ratio: sli ?? 0.99,
+ slo: {
+ stream: name,
+ signal: 'traces' as const,
+ target: 0.999,
+ window_days: 28,
+ sli,
+ budget_remaining_ratio: budget,
+ burn_rates: burns,
+ low_confidence: false,
+ exhaustion_eta_hours: phase === 2 ? 2.3 : null,
+ },
+ })
+
+ return {
+ epoch: EPOCH,
+ streams: {
+ [STREAM_A]: makeStream(STREAM_A),
+ [STREAM_B]: makeStream(STREAM_B),
+ },
+ }
+}
+
+export function mockSlo(): V2SloResponse {
+ const status = mockStatus()
+ const result: V2SloResponse = {}
+ for (const [name, snap] of Object.entries(status.streams)) {
+ result[name] = snap.slo
+ }
+ return result
+}
+
+export function mockSloHistory(): V2SloHistoryResponse {
+ const now = Math.floor(Date.now() / 1000)
+ const buckets: V2SloHistoryResponse = []
+ for (let i = 47; i >= 0; i--) {
+ const t = now - i * 3600
+ const phase = (Math.floor(i / 12) % 4) as SimPhase
+ const sli = getSliForPhase(phase)
+ const good = 3540
+ const bad = sli !== null ? Math.round((1 - sli) * 3600) : 0
+ buckets.push({ bucket_start_s: t, sli, good, bad, unknown: 0 })
+ }
+ return buckets
+}
+
+export function mockEvents(): V2EventsResponse {
+ const phase = getSimPhase()
+ const now = Date.now()
+ const events: RawEvent[] = []
+
+ if (phase >= 1) {
+ events.push({
+ id: 'sim-loss-01',
+ stream: STREAM_A,
+ epoch: EPOCH,
+ class: 'loss' as const,
+ signal: 'traces',
+ window: {
+ from: new Date(now - 15 * 60_000).toISOString(),
+ to: new Date(now - 2 * 60_000).toISOString(),
+ },
+ probes: {
+ sent: 900,
+ verified: 800,
+ missing: 95,
+ duplicate: 5,
+ unknown: 0,
+ },
+ delivery_ratio: 0.889,
+ loss_onset: new Date(now - 15 * 60_000).toISOString(),
+ gap_runs: [
+ {
+ seq_from: 142,
+ seq_to: 196,
+ t_from: new Date(now - 15 * 60_000).toISOString(),
+ t_to: new Date(now - 12 * 60_000).toISOString(),
+ },
+ ],
+ gap_shape: 'contiguous',
+ extrapolated_user_spans_lost: 190,
+ confidence: 'high',
+ traces_filter: `spanledger.stream = '${STREAM_A}' AND spanledger.seq >= 142 AND spanledger.seq <= 196`,
+ })
+ }
+
+ if (phase === 3) {
+ events.unshift({
+ id: 'sim-recovery-01',
+ class: 'recovery' as const,
+ stream: STREAM_A,
+ signal: 'traces',
+ epoch: EPOCH,
+ window_from_ns: (now - 10 * 60_000) * 1_000_000,
+ window_to_ns: (now - 1 * 60_000) * 1_000_000,
+ window: {
+ from: new Date(now - 10 * 60_000).toISOString(),
+ to: new Date(now - 1 * 60_000).toISOString(),
+ },
+ severity: 'info' as const,
+ payload: { recovered_after_windows: 2, delivery_ratio: 0.9998 },
+ links: ['sim-loss-01'],
+ emitted_at_ns: (now - 60_000) * 1_000_000,
+ })
+ }
+
+ return { events, next_cursor: null }
+}
diff --git a/frontend/src/api/normalize.ts b/frontend/src/api/normalize.ts
new file mode 100644
index 0000000..a5b1661
--- /dev/null
+++ b/frontend/src/api/normalize.ts
@@ -0,0 +1,179 @@
+/**
+ * Event normalizer — folds the two backend wire shapes into one discriminated union.
+ *
+ * Two wire shapes exist (D7, hard backend requirement — do not "fix" this):
+ *
+ * 1. V1 finding classes (loss, entry_refused, backend_unreachable, verification_stalled):
+ * flat payload — the event JSON IS the V1 §4.2 finding object.
+ * `backend_unreachable` uses a minimal ad hoc subset — tolerate missing fields.
+ *
+ * 2. All other classes (recovery, epoch_orphaned, budget_warning, budget_exhausted,
+ * burn_rate_high, deploy_marker): enveloped —
+ * {id, class, stream, signal, epoch, window, severity, payload, links, emitted_at_ns}
+ *
+ * Detection rule: `'severity' in raw && 'emitted_at_ns' in raw` → enveloped; else finding.
+ * (Findings carry RFC3339 windows and no top-level severity.)
+ *
+ * Nothing above the API layer ever sees the raw split — everything is SpanledgerEvent.
+ */
+
+import type {
+ FindingClass,
+ EnvelopedClass,
+ Severity,
+ FindingPayload,
+ RawEnvelopedEvent,
+} from './types'
+
+// ─── Normalized union type ────────────────────────────────────────────────
+
+/** Severity synthesized for finding-class events: loss/backend_unreachable = critical, others = warning */
+const FINDING_SEVERITY: Record = {
+ loss: 'critical',
+ backend_unreachable: 'critical',
+ entry_refused: 'warning',
+ verification_stalled: 'warning',
+}
+
+const V1_FINDING_CLASSES = new Set([
+ 'loss',
+ 'entry_refused',
+ 'backend_unreachable',
+ 'verification_stalled',
+])
+
+export interface NormalizedFindingEvent {
+ kind: 'finding'
+ class: FindingClass
+ id: string
+ stream: string | null
+ signal: string | null
+ epoch: string | null
+ severity: Severity
+ /** Milliseconds UTC — falls back to window.to → window.from → now */
+ emittedAtMs: number
+ finding: FindingPayload
+}
+
+export interface NormalizedEnvelopedEvent {
+ kind: 'enveloped'
+ class: EnvelopedClass
+ id: string
+ stream: string | null
+ signal: string | null
+ epoch: string | null
+ severity: Severity
+ /** Milliseconds UTC from emitted_at_ns */
+ emittedAtMs: number
+ window: { fromMs: number | null; toMs: number | null }
+ payload: Record
+ links: string[]
+}
+
+export type SpanledgerEvent = NormalizedFindingEvent | NormalizedEnvelopedEvent
+
+// ─── RFC3339 → milliseconds ───────────────────────────────────────────────
+
+function rfc3339ToMs(rfc: string | null | undefined): number | null {
+ if (!rfc) return null
+ const ms = Date.parse(rfc)
+ return isNaN(ms) ? null : ms
+}
+
+// ─── Normalizer ───────────────────────────────────────────────────────────
+
+/**
+ * Normalize one raw event from the API into a SpanledgerEvent.
+ * Throws a descriptive error if the raw event is structurally invalid.
+ */
+export function normalizeEvent(raw: unknown): SpanledgerEvent {
+ if (typeof raw !== 'object' || raw === null) {
+ throw new Error('normalizeEvent: expected object, got ' + typeof raw)
+ }
+ const r = raw as Record
+
+ // Detection rule (from FRONTEND_ARCHITECTURE.md):
+ const isEnveloped =
+ 'severity' in r && 'emitted_at_ns' in r && !V1_FINDING_CLASSES.has(r['class'] as string)
+
+ if (isEnveloped) {
+ return normalizeEnveloped(r)
+ }
+ return normalizeFinding(r)
+}
+
+function normalizeFinding(r: Record): NormalizedFindingEvent {
+ // Tolerate missing fields — backend_unreachable uses a minimal payload
+ const eventClass = (r['class'] as FindingClass | undefined) ?? 'loss'
+ const id = (r['id'] as string | undefined) ?? ''
+ const stream = (r['stream'] as string | null | undefined) ?? null
+ const signal = (r['signal'] as string | null | undefined) ?? null
+ const epoch = (r['epoch'] as string | null | undefined) ?? null
+
+ // emittedAtMs: fall back window.to → window.from → now
+ const window_ = r['window'] as { from?: string; to?: string } | undefined
+ const emittedAtMs = rfc3339ToMs(window_?.to) ?? rfc3339ToMs(window_?.from) ?? Date.now()
+
+ return {
+ kind: 'finding',
+ class: eventClass,
+ id,
+ stream,
+ signal,
+ epoch,
+ severity: FINDING_SEVERITY[eventClass] ?? 'warning',
+ emittedAtMs,
+ finding: r as unknown as FindingPayload,
+ }
+}
+
+function normalizeEnveloped(r: Record): NormalizedEnvelopedEvent {
+ const ev = r as unknown as RawEnvelopedEvent
+ const windowRaw = ev.window
+
+ let fromMs: number | null = null
+ let toMs: number | null = null
+
+ if (typeof ev.window_from_ns === 'number') {
+ fromMs = ev.window_from_ns / 1_000_000
+ } else if (windowRaw?.from) {
+ fromMs = rfc3339ToMs(windowRaw.from)
+ }
+
+ if (typeof ev.window_to_ns === 'number') {
+ toMs = ev.window_to_ns / 1_000_000
+ } else if (windowRaw?.to) {
+ toMs = rfc3339ToMs(windowRaw.to)
+ }
+
+ // emitted_at_ns is nanoseconds; convert to milliseconds
+ const emittedAtMs =
+ typeof ev.emitted_at_ns === 'number' ? ev.emitted_at_ns / 1_000_000 : Date.now()
+
+ return {
+ kind: 'enveloped',
+ class: ev.class,
+ id: ev.id ?? '',
+ stream: ev.stream ?? null,
+ signal: ev.signal ?? null,
+ epoch: ev.epoch ?? null,
+ severity: ev.severity,
+ emittedAtMs,
+ window: { fromMs, toMs },
+ payload: ev.payload ?? {},
+ links: ev.links ?? [],
+ }
+}
+
+/** Normalize an array of raw events, skipping any that fail normalization */
+export function normalizeEvents(raws: unknown[]): SpanledgerEvent[] {
+ const result: SpanledgerEvent[] = []
+ for (const raw of raws) {
+ try {
+ result.push(normalizeEvent(raw))
+ } catch {
+ // Skip malformed events — don't crash the whole list
+ }
+ }
+ return result
+}
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
new file mode 100644
index 0000000..5c76782
--- /dev/null
+++ b/frontend/src/api/types.ts
@@ -0,0 +1,229 @@
+/**
+ * API types mirroring `spanledger/httpapi.py` (plan §3.1, module 6).
+ * Hand-written because the backend has no OpenAPI (D3 backend-side).
+ * All shapes verified against httpapi.py on main (post P3-4).
+ *
+ * Two event wire shapes exist (D7, hard backend requirement):
+ * - V1 finding classes (loss, entry_refused, backend_unreachable, verification_stalled):
+ * flat payload — the event JSON IS the V1 §4.2 finding object itself.
+ * - All other classes: enveloped — {id, class, stream, signal, epoch, window, severity, payload, links, emitted_at_ns}
+ *
+ * The normalizer (normalize.ts) folds both into SpanledgerEvent at the boundary.
+ * Nothing above the API layer ever sees the raw split.
+ */
+
+// ─── Primitive types ──────────────────────────────────────────────────────
+
+export type Severity = 'info' | 'warning' | 'critical'
+
+export type SignalType = 'traces' | 'logs' | 'metrics'
+
+// V1 finding classes (flat wire shape)
+export type FindingClass = 'loss' | 'entry_refused' | 'backend_unreachable' | 'verification_stalled'
+
+// V2-only enveloped event classes
+export type EnvelopedClass =
+ | 'recovery'
+ | 'epoch_orphaned'
+ | 'budget_warning'
+ | 'budget_exhausted'
+ | 'burn_rate_high'
+ | 'deploy_marker'
+
+export type EventClass = FindingClass | EnvelopedClass
+
+// ─── GET /healthz ─────────────────────────────────────────────────────────
+
+export interface HealthzResponse {
+ status: 'ok'
+}
+
+// ─── GET /status (V1) ─────────────────────────────────────────────────────
+
+export interface V1StreamSnapshot {
+ sent: number
+ verified: number
+ missing: number
+ duplicate: number
+ delivery_ratio: number
+}
+
+export interface V1StatusResponse {
+ epoch: string
+ streams: Record
+}
+
+// ─── GET /findings (V1) ───────────────────────────────────────────────────
+// Returns array of V1 §4.2 finding objects (same as raw finding wire shape)
+
+export interface GapRun {
+ seq_from: number
+ seq_to: number
+ t_from: string // RFC3339
+ t_to: string // RFC3339
+}
+
+export interface FindingProbes {
+ sent: number
+ verified: number
+ missing: number
+ duplicate: number
+ unknown: number
+}
+
+/** V1 §4.2 finding wire format — also the wire format for V1 finding-class events */
+export interface FindingPayload {
+ id: string
+ stream: string
+ epoch: string
+ class: FindingClass
+ signal: string
+ window: { from: string; to: string } // RFC3339
+ probes: FindingProbes
+ delivery_ratio: number
+ loss_onset?: string // RFC3339, only on 'loss'
+ gap_runs: GapRun[]
+ gap_shape: 'contiguous' | 'striped' | 'scattered'
+ extrapolated_user_spans_lost: number
+ correlation_hint?: string
+ confidence?: string
+ traces_filter?: string
+ // backend_unreachable uses a minimal subset — tolerate missing fields
+ detail?: string
+}
+
+// ─── SLO snapshot shape (SLOEngine.snapshot) ──────────────────────────────
+
+export interface BurnRates {
+ '5m': number
+ '1h': number
+ '6h': number
+ '3d': number
+}
+
+export interface SloSnapshot {
+ stream: string
+ signal: string
+ target: number
+ window_days: number
+ sli: number | null
+ budget_remaining_ratio: number
+ burn_rates: BurnRates
+ low_confidence: boolean
+ exhaustion_eta_hours: number | null
+}
+
+// ─── GET /api/v2/status ───────────────────────────────────────────────────
+
+/** Traces-signal stream snapshot (standard probe model) */
+export interface TracesStreamSnapshot extends V1StreamSnapshot {
+ stream: string
+ signal: string
+ slo: SloSnapshot
+}
+
+/** Metrics-signal snapshot uses max-stagnation model (D16), not probe counts */
+export interface MetricsStreamSnapshot {
+ stream: string
+ signal: string
+ delivery: 'advancing' | 'stalled'
+ prev_max: number
+ observed_max: number
+ slo: SloSnapshot
+}
+
+export type StreamSnapshot = TracesStreamSnapshot | MetricsStreamSnapshot
+
+export interface V2StatusResponse {
+ epoch: string
+ streams: Record
+}
+
+// ─── GET /api/v2/streams/{name} ───────────────────────────────────────────
+
+export type V2StreamResponse = StreamSnapshot
+
+// ─── GET /api/v2/slo ──────────────────────────────────────────────────────
+
+export type V2SloResponse = Record
+
+// ─── GET /api/v2/slo/{stream}/history ─────────────────────────────────────
+
+export interface SloHistoryBucket {
+ bucket_start_s: number
+ sli: number | null
+ good: number
+ bad: number
+ unknown: number
+}
+
+export type V2SloHistoryResponse = SloHistoryBucket[]
+
+// ─── Event wire shapes (raw, before normalization) ────────────────────────
+
+/**
+ * V1 finding-class wire shape: the event JSON IS the finding payload.
+ * Detection: `!('severity' in raw && 'emitted_at_ns' in raw)`
+ */
+export type RawFindingEvent = FindingPayload
+
+/**
+ * Enveloped wire shape for all non-V1-finding classes.
+ * Detection: `'severity' in raw && 'emitted_at_ns' in raw`
+ */
+export interface RawEnvelopedEvent {
+ id: string
+ class: EnvelopedClass
+ stream: string | null
+ signal: string | null
+ epoch?: string | null
+ window?: { from?: string | null; to?: string | null } | null
+ window_from_ns?: number | null
+ window_to_ns?: number | null
+ severity: Severity
+ payload: Record
+ links: string[]
+ emitted_at_ns: number
+}
+
+export type RawEvent = RawFindingEvent | RawEnvelopedEvent
+
+// ─── GET /api/v2/events ───────────────────────────────────────────────────
+
+export interface V2EventsResponse {
+ events: RawEvent[]
+ next_cursor: string | null
+}
+
+// ─── GET /api/v2/events/{id} ──────────────────────────────────────────────
+
+export type V2EventResponse = RawEvent
+
+// ─── POST /api/v2/deploy-markers ─────────────────────────────────────────
+
+export interface DeployMarkerRequest {
+ scope: 'stream' | 'global'
+ stream?: string
+ label: string
+ config_hash?: string
+ at?: string // RFC3339
+}
+
+export interface DeployMarkerResponse {
+ id: string
+ at: string // RFC3339
+ scope: 'stream' | 'global'
+ stream: string | null
+ label: string
+ config_hash: string | null
+ source: string
+}
+
+// ─── RFC 7807 Problem JSON ────────────────────────────────────────────────
+
+export interface ProblemJson {
+ type: string
+ title: string
+ status: number
+ detail?: string
+}
diff --git a/frontend/src/components/charts/SliHistoryChart.tsx b/frontend/src/components/charts/SliHistoryChart.tsx
new file mode 100644
index 0000000..ddd8493
--- /dev/null
+++ b/frontend/src/components/charts/SliHistoryChart.tsx
@@ -0,0 +1,145 @@
+/**
+ * SliHistoryChart — Recharts area chart of SLI vs bucket_start_s.
+ *
+ * DESIGN_SYSTEM.md §Charts / FRONTEND_PHASE_1.md FE-8:
+ * - Target reference line: dashed, semantic color, right-edge label
+ * - Y-domain clamped: [min(target-0.005, dataMin), 1] to magnify excursions
+ * - Gap rendering for null-SLI buckets (connectNulls={false})
+ * - No Recharts mount animations (isAnimationActive={false})
+ * - Grid: --border dashed, horizontal only
+ * - Axis: --text-xs, --text-dim, no axis lines
+ * - Tooltip: --surface-2, border, radius-sm, mono values, 150ms fade
+ * - Area fill: 18%→0 gradient
+ */
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ReferenceLine,
+ ResponsiveContainer,
+ type TooltipProps,
+} from 'recharts'
+import type { SloHistoryBucket } from '@/api/types'
+import { formatSli } from '@/lib/format'
+import { formatAbsolute } from '@/lib/time'
+
+interface SliHistoryChartProps {
+ buckets: SloHistoryBucket[]
+ target: number
+ height?: number
+}
+
+interface ChartPoint {
+ t: number
+ sli: number | null
+}
+
+function CustomTooltip({ active, payload }: TooltipProps) {
+ if (!active || !payload || payload.length === 0) return null
+ const point = payload[0]?.payload as ChartPoint | undefined
+ if (!point) return null
+
+ return (
+
+
{formatAbsolute(point.t * 1000)}
+
+ SLI: {formatSli(point.sli)}
+
+
+ )
+}
+
+export function SliHistoryChart({ buckets, target, height = 160 }: SliHistoryChartProps) {
+ const data: ChartPoint[] = buckets.map((b) => ({ t: b.bucket_start_s, sli: b.sli }))
+
+ const validSlis = data.filter((d) => d.sli !== null).map((d) => d.sli as number)
+ const dataMin = validSlis.length > 0 ? Math.min(...validSlis) : target
+ const yMin = Math.min(target - 0.005, dataMin)
+ // Clamp: never below target-0.01, never above 1
+ const yDomain = [Math.max(yMin, target - 0.01), 1] as [number, number]
+
+ const gradientId = 'sliAreaGradient'
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const d = new Date(v * 1000)
+ return d.toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ })
+ }}
+ tick={{ fill: 'var(--text-dim)', fontSize: 11 }}
+ axisLine={false}
+ tickLine={false}
+ />
+
+ `${(v * 100).toFixed(2)}%`}
+ tick={{ fill: 'var(--text-dim)', fontSize: 11 }}
+ axisLine={false}
+ tickLine={false}
+ width={52}
+ />
+
+ } />
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/charts/TimelineAxis.tsx b/frontend/src/components/charts/TimelineAxis.tsx
new file mode 100644
index 0000000..a12ec9d
--- /dev/null
+++ b/frontend/src/components/charts/TimelineAxis.tsx
@@ -0,0 +1,126 @@
+/**
+ * TimelineAxis and DeployMarkerLane — shared time-axis primitives for the
+ * Deploys screen and any multi-lane timeline rendering.
+ *
+ * TimelineAxis: SVG horizontal axis with time ticks.
+ * DeployMarkerLane: markers (deploy events) positioned on the same axis.
+ */
+import type { ReactNode } from 'react'
+
+interface TimeRange {
+ fromMs: number
+ toMs: number
+}
+
+interface TimelineAxisProps {
+ range: TimeRange
+ width?: number
+ height?: number
+ ticks?: number
+}
+
+export function TimelineAxis({ range, height = 32, ticks = 5 }: TimelineAxisProps) {
+ const tickTimes: number[] = []
+ const span = range.toMs - range.fromMs
+ const step = span / ticks
+
+ for (let i = 0; i <= ticks; i++) {
+ tickTimes.push(range.fromMs + i * step)
+ }
+
+ return (
+
+
+ {/* Axis line */}
+
+ {/* Ticks */}
+ {tickTimes.map((t, i) => {
+ const pct = ((t - range.fromMs) / (range.toMs - range.fromMs)) * 100
+ return (
+
+
+
+ {new Date(t).toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ })}
+
+
+ )
+ })}
+
+
+ )
+}
+
+interface DeployMarker {
+ id: string
+ atMs: number
+ label: string
+}
+
+interface DeployMarkerLaneProps {
+ markers: DeployMarker[]
+ range: TimeRange
+ height?: number
+ renderTooltip?: (marker: DeployMarker) => ReactNode
+}
+
+export function DeployMarkerLane({ markers, range, height = 24 }: DeployMarkerLaneProps) {
+ return (
+
+
+ {markers.map((m) => {
+ const span = range.toMs - range.fromMs || 1
+ const pct = ((m.atMs - range.fromMs) / span) * 100
+
+ // Skip markers outside range
+ if (pct < 0 || pct > 100) return null
+
+ return (
+
+ {m.label}
+ {/* Vertical line */}
+
+ {/* Diamond marker */}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/frontend/src/components/events/EventRow.tsx b/frontend/src/components/events/EventRow.tsx
new file mode 100644
index 0000000..da86997
--- /dev/null
+++ b/frontend/src/components/events/EventRow.tsx
@@ -0,0 +1,148 @@
+import type { SpanledgerEvent } from '@/api/normalize'
+import { EventClassBadge } from '@/components/ui/Badge'
+import { formatRelative, formatAbsolute } from '@/lib/time'
+import { eventHeadline } from '@/lib/format'
+import { Tooltip } from '@/components/ui/Tooltip'
+import { useMemo, useEffect } from 'react'
+
+interface EventRowProps {
+ event: SpanledgerEvent
+ compact?: boolean
+ onClick?: () => void
+ highlighted?: boolean
+}
+
+export function EventRow({
+ event,
+ compact = false,
+ onClick,
+ highlighted = false,
+}: EventRowProps): React.ReactElement {
+ useEffect(() => {
+ if (highlighted) {
+ const el = document.getElementById(`event-row-${event.id}`)
+ if (el) {
+ // Subtle delay to ensure it renders first
+ const timer = setTimeout(() => {
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' })
+ }, 100)
+ return () => clearTimeout(timer)
+ }
+ }
+ }, [highlighted, event.id])
+
+ const severityColor = useMemo(() => {
+ switch (event.severity) {
+ case 'critical':
+ return 'var(--crit)'
+ case 'warning':
+ return 'var(--warn)'
+ case 'info':
+ return 'var(--info)'
+ default:
+ return 'var(--unknown)'
+ }
+ }, [event.severity])
+
+ const timeLabel = useMemo(() => {
+ return formatRelative(event.emittedAtMs)
+ }, [event.emittedAtMs])
+
+ const absoluteTime = useMemo(() => {
+ return formatAbsolute(event.emittedAtMs)
+ }, [event.emittedAtMs])
+
+ const headline = useMemo(() => {
+ return eventHeadline(event)
+ }, [event])
+
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ onClick()
+ }
+ }
+ : undefined
+ }
+ >
+
+ {/* Left severity rail */}
+
+
+ {/* Main event content */}
+
+
+
+
+ {event.stream && (
+
+ {event.stream}
+
+ )}
+ {event.signal && !compact && (
+
+ {event.signal}
+
+ )}
+
+
+ {headline}
+
+
+
+ {/* Right time info */}
+
+
+
+ {timeLabel}
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/events/GapRunChart.tsx b/frontend/src/components/events/GapRunChart.tsx
new file mode 100644
index 0000000..59521de
--- /dev/null
+++ b/frontend/src/components/events/GapRunChart.tsx
@@ -0,0 +1,129 @@
+/**
+ * GapRunChart — SVG rendering of gap runs as seq-range bands on a time axis.
+ * Used in Incident Details to show the structure of a loss event.
+ */
+import type { GapRun } from '@/api/types'
+
+interface GapRunChartProps {
+ gapRuns: GapRun[]
+ width?: number
+ height?: number
+}
+
+export function GapRunChart({ gapRuns, height = 48 }: GapRunChartProps) {
+ if (gapRuns.length === 0) {
+ return (
+
+ No gap runs
+
+ )
+ }
+
+ // Compute time range across all runs
+ const allFromMs = gapRuns.map((r) => new Date(r.t_from).getTime())
+ const allToMs = gapRuns.map((r) => new Date(r.t_to).getTime())
+ const minT = Math.min(...allFromMs)
+ const maxT = Math.max(...allToMs)
+ const rangeT = maxT - minT || 1
+
+ // Compute seq range
+ const allSeqFrom = gapRuns.map((r) => r.seq_from)
+ const allSeqTo = gapRuns.map((r) => r.seq_to)
+ const minSeq = Math.min(...allSeqFrom)
+ const maxSeq = Math.max(...allSeqTo)
+ const padding = { left: 8, right: 8, top: 4, bottom: 4 }
+
+ return (
+
+
+ {gapRuns.map((run, i) => {
+ const fromMs = new Date(run.t_from).getTime()
+ const toMs = new Date(run.t_to).getTime()
+
+ const x1 =
+ padding.left + ((fromMs - minT) / rangeT) * (100 - padding.left - padding.right)
+ const x2 = padding.left + ((toMs - minT) / rangeT) * (100 - padding.left - padding.right)
+ const width = Math.max(x2 - x1, 2) // minimum 2% width for visibility
+
+ return (
+
+
+ seq {run.seq_from}–{run.seq_to} ({run.t_from} → {run.t_to})
+
+
+
+ )
+ })}
+
+ {/* Seq range labels */}
+
+ seq {minSeq}
+
+
+ {maxSeq}
+
+
+ )
+}
+
+/**
+ * ProbeCountsBar — stacked verified/missing/duplicate/unknown bar.
+ */
+interface ProbeCounts {
+ sent: number
+ verified: number
+ missing: number
+ duplicate: number
+ unknown: number
+}
+
+export function ProbeCountsBar({ counts }: { counts: ProbeCounts }) {
+ const total = counts.sent || 1
+ const segments = [
+ { label: 'verified', value: counts.verified, color: 'var(--ok)' },
+ { label: 'missing', value: counts.missing, color: 'var(--crit)' },
+ { label: 'duplicate', value: counts.duplicate, color: 'var(--warn)' },
+ { label: 'unknown', value: counts.unknown, color: 'var(--unknown)' },
+ ]
+
+ return (
+
+ {segments.map(({ label, value, color }) => {
+ const pct = (value / total) * 100
+ if (pct === 0) return null
+ return
+ })}
+
+ )
+}
diff --git a/frontend/src/components/layout/ConnectionBanner.tsx b/frontend/src/components/layout/ConnectionBanner.tsx
new file mode 100644
index 0000000..c95dac8
--- /dev/null
+++ b/frontend/src/components/layout/ConnectionBanner.tsx
@@ -0,0 +1,88 @@
+import { AlertOctagon, RefreshCw } from 'lucide-react'
+import { useHealthz } from '@/api/hooks'
+import { useEffect, useRef, useState } from 'react'
+import { useUi } from '@/providers/UiProvider'
+import { formatRelative } from '@/lib/time'
+
+/**
+ * ConnectionBanner — slim persistent banner shown when the backend is unreachable.
+ *
+ * Appears after ≥ 2 consecutive healthz failures (connection error, not just 1 blip).
+ * The TopBar handles the connection dot; this banner is the user action surface.
+ * Never used for poll failures — this is the honesty feature.
+ */
+export function ConnectionBanner() {
+ const { error: healthzError, dataUpdatedAt, refetch } = useHealthz()
+ const { setConnectionDown } = useUi()
+
+ const failCountRef = useRef(0)
+ const [isDown, setIsDown] = useState(false)
+ const [retrying, setRetrying] = useState(false)
+
+ useEffect(() => {
+ if (healthzError) {
+ failCountRef.current += 1
+ if (failCountRef.current >= 2) {
+ setIsDown(true)
+ setConnectionDown(true)
+ }
+ } else {
+ failCountRef.current = 0
+ setIsDown(false)
+ setConnectionDown(false)
+ }
+ }, [healthzError, setConnectionDown])
+
+ const handleRetry = async () => {
+ setRetrying(true)
+ try {
+ await refetch()
+ } finally {
+ setRetrying(false)
+ }
+ }
+
+ if (!isDown) return null
+
+ const lastDataAge = dataUpdatedAt > 0 ? formatRelative(dataUpdatedAt) : 'unknown'
+
+ return (
+
+
+
+
+ Can't reach spanLedger at :8231 — retrying
+ {dataUpdatedAt > 0 && (
+ (last data {lastDataAge})
+ )}
+
+
+
+
void handleRetry()}
+ disabled={retrying}
+ className="flex items-center gap-1.5 px-3 py-1 rounded-sm text-xs font-medium"
+ style={{
+ backgroundColor: 'var(--crit-bg)',
+ color: 'var(--crit)',
+ border: '1px solid var(--crit)',
+ cursor: retrying ? 'default' : 'pointer',
+ opacity: retrying ? 0.7 : 1,
+ }}
+ aria-label="Retry connection"
+ >
+
+ Retry
+
+
+ )
+}
diff --git a/frontend/src/components/layout/PageHeader.tsx b/frontend/src/components/layout/PageHeader.tsx
new file mode 100644
index 0000000..876e574
--- /dev/null
+++ b/frontend/src/components/layout/PageHeader.tsx
@@ -0,0 +1,38 @@
+import type { ReactNode } from 'react'
+
+interface PageHeaderProps {
+ title: string
+ /** Optional right-side slot (signal selector, actions) */
+ actions?: ReactNode
+ /** Optional subtitle / description */
+ description?: string
+}
+
+/**
+ * PageHeader — consistent h1 + optional actions row for every page.
+ * One h1 per page (accessibility rule). Title uses --text-xl tokens.
+ */
+export function PageHeader({ title, actions, description }: PageHeaderProps) {
+ return (
+
+
+
+ {title}
+
+ {actions &&
{actions}
}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/layout/RootLayout.tsx b/frontend/src/components/layout/RootLayout.tsx
new file mode 100644
index 0000000..d8b9d7a
--- /dev/null
+++ b/frontend/src/components/layout/RootLayout.tsx
@@ -0,0 +1,38 @@
+import { Outlet } from 'react-router-dom'
+import { Sidebar } from './Sidebar'
+import { TopBar } from './TopBar'
+import { ConnectionBanner } from './ConnectionBanner'
+import { ShortcutsModal } from '@/components/ui/ShortcutsModal'
+import { useShortcuts } from '@/hooks/useShortcuts'
+
+export function RootLayout() {
+ const { shortcutsModalOpen, closeShortcutsModal } = useShortcuts()
+
+ return (
+
+ )
+}
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..96b62f1
--- /dev/null
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -0,0 +1,159 @@
+import { NavLink, useLocation } from 'react-router-dom'
+import {
+ LayoutDashboard,
+ ShieldCheck,
+ List,
+ Rocket,
+ GitBranch,
+ Scale,
+ Settings2,
+ Presentation,
+ ChevronLeft,
+ ChevronRight,
+} from 'lucide-react'
+import { useState } from 'react'
+import { useUi } from '@/providers/UiProvider'
+
+interface NavItem {
+ to: string
+ label: string
+ Icon: React.ComponentType<{ size?: number; strokeWidth?: number; style?: React.CSSProperties }>
+}
+
+const NAV_ITEMS: NavItem[] = [
+ { to: '/', label: 'Overview', Icon: LayoutDashboard },
+ { to: '/reliability', label: 'Reliability', Icon: ShieldCheck },
+ { to: '/timeline', label: 'Event Timeline', Icon: List },
+ { to: '/deploys', label: 'Deploys', Icon: Rocket },
+ { to: '/streams', label: 'Streams', Icon: GitBranch },
+ { to: '/ledger', label: 'Conservation Ledger', Icon: Scale },
+ { to: '/settings', label: 'Settings', Icon: Settings2 },
+ { to: '/demo', label: 'Demo Mode', Icon: Presentation },
+]
+
+export function Sidebar() {
+ const [collapsed, setCollapsed] = useState(false)
+ const { presentationMode, mobileMenuOpen, setMobileMenuOpen } = useUi()
+ const location = useLocation()
+
+ const isCollapsed = collapsed || presentationMode
+ const width = isCollapsed ? 'var(--sidebar-icon-width)' : 'var(--sidebar-width)'
+
+ return (
+ <>
+ {/* Mobile backdrop overlay */}
+ {mobileMenuOpen && (
+ setMobileMenuOpen(false)}
+ />
+ )}
+
+
+ {/* Logo / brand */}
+
+
+ {isCollapsed ? 'SL' : 'spanLedger'}
+
+
+
+ {/* Nav items */}
+
+ {NAV_ITEMS.map(({ to, label, Icon }) => {
+ // Special case: /streams/* should match active for stream nav
+ const isActive = to === '/' ? location.pathname === '/' : location.pathname.startsWith(to)
+
+ return (
+
+ setMobileMenuOpen(false)}
+ className="flex items-center gap-3 px-3 py-2 rounded-sm text-sm font-medium whitespace-nowrap"
+ style={({ isActive: routerActive }) => {
+ const active = isActive || routerActive
+ return {
+ color: active ? 'var(--text)' : 'var(--text-dim)',
+ backgroundColor: active ? 'var(--accent-bg)' : 'transparent',
+ transition: `color var(--transition-fast), background-color var(--transition-fast)`,
+ }
+ }}
+ aria-label={isCollapsed ? label : undefined}
+ title={isCollapsed ? label : undefined}
+ >
+ {({ isActive: routerActive }) => {
+ const active = isActive || routerActive
+ return (
+ <>
+
+ {!isCollapsed && (
+
+ {label}
+ {to === '/ledger' && (
+
+ soon
+
+ )}
+
+ )}
+ >
+ )
+ }}
+
+
+ )
+ })}
+
+
+ {/* Collapse toggle */}
+
+ setCollapsed((c) => !c)}
+ className="flex items-center justify-center w-full h-8 rounded-sm"
+ style={{
+ color: 'var(--text-faint)',
+ backgroundColor: 'transparent',
+ border: 'none',
+ cursor: 'pointer',
+ transition: `color var(--transition-fast)`,
+ }}
+ aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
+ title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
+ >
+ {isCollapsed ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx
new file mode 100644
index 0000000..0334824
--- /dev/null
+++ b/frontend/src/components/layout/TopBar.tsx
@@ -0,0 +1,101 @@
+import { History, Menu } from 'lucide-react'
+import { useHealthz } from '@/api/hooks'
+import { useStatus } from '@/api/hooks'
+import { useUi } from '@/providers/UiProvider'
+import { formatRelative } from '@/lib/time'
+import { StatusDot } from '@/components/ui/StatusDot'
+import { TourPopover } from '@/components/ui/TourPopover'
+import { isSimulatedMode } from '@/api/client'
+
+export function TopBar() {
+ const { data: healthz, error: healthzError, dataUpdatedAt } = useHealthz()
+ const { signal, setMobileMenuOpen } = useUi()
+ const isSimulated = isSimulatedMode()
+ const { data: status } = useStatus(signal)
+
+ const isConnected = !healthzError && healthz?.status === 'ok'
+ const lastUpdateMs = dataUpdatedAt > 0 ? dataUpdatedAt : null
+ const ageMs = lastUpdateMs ? Date.now() - lastUpdateMs : null
+
+ // Age indicator: amber past 15s, red past 60s
+ let ageCss = 'var(--text-faint)'
+ if (ageMs !== null) {
+ if (ageMs > 60_000) ageCss = 'var(--crit)'
+ else if (ageMs > 15_000) ageCss = 'var(--warn)'
+ }
+
+ return (
+
+ {/* Left: mobile menu button + epoch info */}
+
+
setMobileMenuOpen(true)}
+ className="md:hidden p-1.5 rounded-sm hover:bg-surface-2 focus:outline-none"
+ style={{ color: 'var(--text)', border: 'none', background: 'none', cursor: 'pointer' }}
+ aria-label="Open mobile navigation menu"
+ >
+
+
+
+ {status?.epoch && (
+
+
+
+ epoch:
+
+ {status.epoch.slice(0, 8)}…
+
+ )}
+
+
+ {/* Right: connection status + freshness + tour */}
+
+
+ {/* Freshness age */}
+ {lastUpdateMs && (
+
+ {formatRelative(lastUpdateMs)}
+
+ )}
+
+ {/* SIMULATED badge — shown when mock data is active */}
+ {/* Populated by mock client activation — see FE-9 */}
+
+ SIMULATED
+
+
+ {/* Connection status dot */}
+
+
+
+ {isConnected ? 'Connected' : 'Disconnected'}
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/slo/BudgetGauge.tsx b/frontend/src/components/slo/BudgetGauge.tsx
new file mode 100644
index 0000000..e45198e
--- /dev/null
+++ b/frontend/src/components/slo/BudgetGauge.tsx
@@ -0,0 +1,130 @@
+/**
+ * BudgetGauge — hand-rolled SVG arc showing budget remaining ratio.
+ *
+ * DESIGN_SYSTEM.md:
+ * - Color by budgetStatus (ok=green, warning/low=yellow, exhausted=red)
+ * - Animated only on value change (not on mount replays)
+ * - Reduced motion: no animation
+ * - sizes: 'sm' (64px) for stream cards, 'lg' (120px) for hero
+ */
+import { useEffect, useRef } from 'react'
+import { budgetStatus } from '@/lib/slo'
+import { formatRatio } from '@/lib/format'
+import { useUi } from '@/providers/UiProvider'
+
+interface BudgetGaugeProps {
+ ratio: number
+ size?: 'sm' | 'lg'
+ showLabel?: boolean
+}
+
+const SIZE_MAP = {
+ sm: { diameter: 64, strokeWidth: 6, fontSize: 13 },
+ lg: { diameter: 120, strokeWidth: 10, fontSize: 20 },
+}
+
+function statusColor(ratio: number): string {
+ const s = budgetStatus(ratio)
+ if (s === 'exhausted') return 'var(--crit)'
+ if (s === 'low' || s === 'warning') return 'var(--warn)'
+ return 'var(--ok)'
+}
+
+export function BudgetGauge({ ratio, size = 'lg', showLabel = true }: BudgetGaugeProps) {
+ const { reducedMotion } = useUi()
+ const { diameter, strokeWidth, fontSize } = SIZE_MAP[size]
+ const radius = (diameter - strokeWidth) / 2
+ const cx = diameter / 2
+ const cy = diameter / 2
+
+ // Arc: 270deg sweep (from 135deg to 405deg, i.e., bottom-left start)
+ const startAngle = 135
+ const sweepAngle = 270
+ const clampedRatio = Math.max(0, Math.min(1, ratio))
+ const arcAngle = clampedRatio * sweepAngle
+
+ function polarToCartesian(angle: number) {
+ const rad = ((angle - 90) * Math.PI) / 180
+ return {
+ x: cx + radius * Math.cos(rad),
+ y: cy + radius * Math.sin(rad),
+ }
+ }
+
+ function arcPath(startDeg: number, endDeg: number) {
+ const s = polarToCartesian(startDeg)
+ const e = polarToCartesian(endDeg)
+ const largeArc = endDeg - startDeg > 180 ? 1 : 0
+ return `M ${s.x} ${s.y} A ${radius} ${radius} 0 ${largeArc} 1 ${e.x} ${e.y}`
+ }
+
+ const bgPath = arcPath(startAngle, startAngle + sweepAngle)
+ const fgPath = arcAngle > 0 ? arcPath(startAngle, startAngle + arcAngle) : null
+
+ const fgRef = useRef
(null)
+ const prevRatioRef = useRef(ratio)
+
+ // Animate arc on value change (not mount)
+ useEffect(() => {
+ if (reducedMotion || prevRatioRef.current === ratio) {
+ prevRatioRef.current = ratio
+ return
+ }
+ prevRatioRef.current = ratio
+ // CSS transition handles the stroke-dasharray change
+ }, [ratio, reducedMotion])
+
+ const color = statusColor(ratio)
+ const isExhausted = ratio <= 0
+
+ return (
+
+
+ {/* Background track */}
+
+ {/* Filled arc */}
+ {fgPath && (
+
+ )}
+
+ {/* Center label */}
+ {showLabel && (
+
+ {isExhausted ? (
+ overspent
+ ) : (
+ {formatRatio(ratio)}
+ )}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/slo/BurnRateBars.tsx b/frontend/src/components/slo/BurnRateBars.tsx
new file mode 100644
index 0000000..6972523
--- /dev/null
+++ b/frontend/src/components/slo/BurnRateBars.tsx
@@ -0,0 +1,95 @@
+/**
+ * BurnRateBars — 4 burn-rate windows displayed as bars vs thresholds.
+ *
+ * DESIGN_SYSTEM.md / FRONTEND_PHASE_1.md FE-8:
+ * - 4 windows: 5m, 1h, 6h, 3d vs thresholds (FAST_BURN=14.4, SLOW_BURN=6.0)
+ * - Log-ish scale capped at 20, with overflow marker
+ */
+import { burnStatus, FAST_BURN, SLOW_BURN } from '@/lib/slo'
+import type { BurnRates } from '@/api/types'
+
+const CAP = 20
+const WINDOWS = ['5m', '1h', '6h', '3d'] as const
+
+interface BurnRateBarsProps {
+ burnRates: BurnRates
+ compact?: boolean
+}
+
+export function BurnRateBars({ burnRates, compact = false }: BurnRateBarsProps) {
+ return (
+
+ {WINDOWS.map((window) => {
+ const rate = burnRates[window] ?? 0
+ const status = burnStatus(window, rate)
+ const overflowed = rate > CAP
+ const displayRate = Math.min(rate, CAP)
+ const pct = (displayRate / CAP) * 100
+
+ const threshold = window === '1h' || window === '5m' ? FAST_BURN : SLOW_BURN
+ const thresholdPct = (threshold / CAP) * 100
+
+ const color =
+ status === 'critical' ? 'var(--crit)' : status === 'warning' ? 'var(--warn)' : 'var(--ok)'
+
+ return (
+
+ {/* Bar */}
+
+ {/* Threshold line */}
+
+ {/* Fill bar */}
+
+ {/* Overflow marker */}
+ {overflowed && (
+
+ ↑
+
+ )}
+
+ {/* Label */}
+
+ {window}
+
+
+ )
+ })}
+
+ )
+}
diff --git a/frontend/src/components/slo/ConfidenceChip.tsx b/frontend/src/components/slo/ConfidenceChip.tsx
new file mode 100644
index 0000000..680fb6e
--- /dev/null
+++ b/frontend/src/components/slo/ConfidenceChip.tsx
@@ -0,0 +1,28 @@
+/**
+ * ConfidenceChip — renders only when low_confidence is true.
+ * Uses --unknown (honesty hue) per design system.
+ */
+import { HelpCircle } from 'lucide-react'
+
+interface ConfidenceChipProps {
+ lowConfidence: boolean
+}
+
+export function ConfidenceChip({ lowConfidence }: ConfidenceChipProps) {
+ if (!lowConfidence) return null
+
+ return (
+
+
+ Low confidence
+
+ )
+}
diff --git a/frontend/src/components/slo/EtaChip.tsx b/frontend/src/components/slo/EtaChip.tsx
new file mode 100644
index 0000000..db3ef94
--- /dev/null
+++ b/frontend/src/components/slo/EtaChip.tsx
@@ -0,0 +1,33 @@
+/**
+ * EtaChip — renders only when exhaustion_eta_hours is not null.
+ * Shows time until budget exhaustion at current burn rate.
+ */
+import { Flame } from 'lucide-react'
+import { formatDurationHours } from '@/lib/format'
+
+interface EtaChipProps {
+ exhaustionEtaHours: number | null
+}
+
+export function EtaChip({ exhaustionEtaHours }: EtaChipProps) {
+ if (exhaustionEtaHours === null) return null
+
+ const isImminent = exhaustionEtaHours < 2
+ const color = isImminent ? 'var(--crit)' : 'var(--warn)'
+ const bg = isImminent ? 'var(--crit-bg)' : 'var(--warn-bg)'
+
+ return (
+
+
+ {exhaustionEtaHours < 0 ? 'Overdrawn' : `ETA ${formatDurationHours(exhaustionEtaHours)}`}
+
+ )
+}
diff --git a/frontend/src/components/slo/SliStat.tsx b/frontend/src/components/slo/SliStat.tsx
new file mode 100644
index 0000000..7764dbb
--- /dev/null
+++ b/frontend/src/components/slo/SliStat.tsx
@@ -0,0 +1,41 @@
+/**
+ * SliStat — displays an SLI value with target context.
+ * Used in Overview stream cards and Reliability Summary.
+ */
+import { formatSli } from '@/lib/format'
+import { useAnimatedNumber } from '@/hooks/useAnimatedNumber'
+
+interface SliStatProps {
+ sli: number | null
+ target: number
+ /** Size variant */
+ size?: 'sm' | 'lg'
+}
+
+export function SliStat({ sli, target, size = 'lg' }: SliStatProps) {
+ const animatedSli = useAnimatedNumber(sli)
+ const isBelowTarget = sli !== null && sli < target
+ const valueColor = isBelowTarget ? 'var(--crit)' : 'var(--ok)'
+
+ const fontSize = size === 'lg' ? 'var(--text-stat-size)' : 'var(--text-xl-size)'
+
+ return (
+
+
+ {formatSli(animatedSli)}
+
+
+ target {formatSli(target)}
+
+
+ )
+}
diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx
new file mode 100644
index 0000000..507ba17
--- /dev/null
+++ b/frontend/src/components/ui/Badge.tsx
@@ -0,0 +1,152 @@
+/**
+ * Badge — semantic colored chip (12% alpha bg + solid text).
+ * SeverityBadge — maps severity string to Badge variant.
+ * EventClassBadge — maps event class string to Badge with canonical icon.
+ */
+import type { ReactNode } from 'react'
+import { TrendingDown, Undo2, History, AlertOctagon, Gauge, Flame, Rocket } from 'lucide-react'
+
+/* ─── Variant definitions ────────────────────────────────────────────────── */
+
+type BadgeVariant = 'ok' | 'warn' | 'crit' | 'info' | 'unknown' | 'accent' | 'neutral'
+
+const variantStyles: Record = {
+ ok: { bg: 'var(--ok-bg)', color: 'var(--ok)' },
+ warn: { bg: 'var(--warn-bg)', color: 'var(--warn)' },
+ crit: { bg: 'var(--crit-bg)', color: 'var(--crit)' },
+ info: { bg: 'var(--info-bg)', color: 'var(--info)' },
+ unknown: { bg: 'var(--unknown-bg)', color: 'var(--unknown)' },
+ accent: { bg: 'var(--accent-bg)', color: 'var(--accent)' },
+ neutral: { bg: 'var(--surface-2)', color: 'var(--text-dim)' },
+}
+
+/* ─── Badge ──────────────────────────────────────────────────────────────── */
+
+interface BadgeProps {
+ variant: BadgeVariant
+ children: ReactNode
+ icon?: ReactNode
+}
+
+export function Badge({ variant, children, icon }: BadgeProps): React.ReactElement {
+ const { bg, color } = variantStyles[variant]
+ return (
+
+ {icon !== undefined && (
+
+ {icon}
+
+ )}
+ {children}
+
+ )
+}
+
+/* ─── SeverityBadge ──────────────────────────────────────────────────────── */
+
+type Severity = 'info' | 'warning' | 'critical'
+
+const severityToVariant: Record = {
+ info: 'info',
+ warning: 'warn',
+ critical: 'crit',
+}
+
+interface SeverityBadgeProps {
+ severity: Severity
+}
+
+export function SeverityBadge({ severity }: SeverityBadgeProps): React.ReactElement {
+ return (
+
+ {severity.charAt(0).toUpperCase() + severity.slice(1)}
+
+ )
+}
+
+/* ─── EventClassBadge ────────────────────────────────────────────────────── */
+
+type EventClassDef = { variant: BadgeVariant; icon: ReactNode | null; label: string }
+
+const ICON_SIZE = 14
+const STROKE = 1.75
+
+const eventClassMap: Record = {
+ loss: {
+ variant: 'crit',
+ icon: ,
+ label: 'Loss',
+ },
+ recovery: {
+ variant: 'ok',
+ icon: ,
+ label: 'Recovery',
+ },
+ epoch_orphaned: {
+ variant: 'info',
+ icon: ,
+ label: 'Epoch orphaned',
+ },
+ backend_unreachable: {
+ variant: 'crit',
+ icon: ,
+ label: 'Backend unreachable',
+ },
+ budget_warning: {
+ variant: 'warn',
+ icon: ,
+ label: 'Budget warning',
+ },
+ budget_exhausted: {
+ variant: 'crit',
+ icon: ,
+ label: 'Budget exhausted',
+ },
+ burn_rate_high: {
+ variant: 'crit',
+ icon: ,
+ label: 'Burn rate high',
+ },
+ deploy_marker: {
+ variant: 'info',
+ icon: ,
+ label: 'Deploy marker',
+ },
+}
+
+interface EventClassBadgeProps {
+ class_: string
+}
+
+export function EventClassBadge({ class_ }: EventClassBadgeProps): React.ReactElement {
+ const def = eventClassMap[class_]
+
+ if (def === undefined) {
+ return {class_}
+ }
+
+ return (
+
+ {def.label}
+
+ )
+}
diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx
new file mode 100644
index 0000000..de7e61d
--- /dev/null
+++ b/frontend/src/components/ui/Button.tsx
@@ -0,0 +1,190 @@
+/**
+ * Button — primary / subtle / ghost variants with loading state.
+ * Follows design system: accent bg primary, surface-2 subtle, ghost text-only.
+ */
+import type { ReactNode, MouseEventHandler } from 'react'
+
+type ButtonVariant = 'primary' | 'subtle' | 'ghost'
+type ButtonSize = 'default' | 'sm'
+
+interface ButtonProps {
+ variant?: ButtonVariant
+ size?: ButtonSize
+ loading?: boolean
+ disabled?: boolean
+ onClick?: MouseEventHandler
+ children: ReactNode
+ className?: string
+ type?: 'button' | 'submit'
+ icon?: ReactNode
+}
+
+/* ─── Inline spinner ─────────────────────────────────────────────────────── */
+
+function Spinner(): React.ReactElement {
+ return (
+
+
+
+
+ )
+}
+
+/* ─── Variant + size styles ──────────────────────────────────────────────── */
+
+function getVariantStyle(variant: ButtonVariant): React.CSSProperties {
+ switch (variant) {
+ case 'primary':
+ return {
+ backgroundColor: 'var(--accent)',
+ color: 'white',
+ border: '1px solid transparent',
+ }
+ case 'subtle':
+ return {
+ backgroundColor: 'var(--surface-2)',
+ color: 'var(--text)',
+ border: '1px solid var(--border-strong)',
+ }
+ case 'ghost':
+ return {
+ backgroundColor: 'transparent',
+ color: 'var(--text)',
+ border: '1px solid transparent',
+ }
+ }
+}
+
+function getSizeStyle(size: ButtonSize): React.CSSProperties {
+ switch (size) {
+ case 'default':
+ return {
+ height: '36px',
+ paddingLeft: '14px',
+ paddingRight: '14px',
+ fontSize: 'var(--text-sm-size)',
+ }
+ case 'sm':
+ return {
+ height: '28px',
+ paddingLeft: '10px',
+ paddingRight: '10px',
+ fontSize: 'var(--text-xs-size)',
+ }
+ }
+}
+
+/* ─── Button ─────────────────────────────────────────────────────────────── */
+
+export function Button({
+ variant = 'subtle',
+ size = 'default',
+ loading = false,
+ disabled = false,
+ onClick,
+ children,
+ className,
+ type = 'button',
+ icon,
+}: ButtonProps): React.ReactElement {
+ const isDisabled = disabled || loading
+
+ const baseStyle: React.CSSProperties = {
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: '6px',
+ borderRadius: 'var(--radius-sm)',
+ fontWeight: 500,
+ fontFamily: 'var(--font-sans)',
+ lineHeight: 1,
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
+ opacity: isDisabled ? 0.5 : 1,
+ pointerEvents: isDisabled ? 'none' : 'auto',
+ transition: `background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast)`,
+ outline: 'none',
+ whiteSpace: 'nowrap',
+ userSelect: 'none',
+ ...getVariantStyle(variant),
+ ...getSizeStyle(size),
+ }
+
+ return (
+ {
+ if (isDisabled) return
+ const el = e.currentTarget
+ if (variant === 'primary') {
+ el.style.backgroundColor = 'var(--accent-hover)'
+ } else if (variant === 'ghost') {
+ el.style.backgroundColor = 'var(--surface)'
+ } else if (variant === 'subtle') {
+ el.style.borderColor = 'var(--border-strong)'
+ el.style.backgroundColor = 'var(--border)'
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (isDisabled) return
+ const el = e.currentTarget
+ if (variant === 'primary') {
+ el.style.backgroundColor = 'var(--accent)'
+ } else if (variant === 'ghost') {
+ el.style.backgroundColor = 'transparent'
+ } else if (variant === 'subtle') {
+ el.style.borderColor = 'var(--border-strong)'
+ el.style.backgroundColor = 'var(--surface-2)'
+ }
+ }}
+ onMouseDown={(e) => {
+ if (isDisabled) return
+ if (variant === 'primary') {
+ e.currentTarget.style.backgroundColor = 'var(--accent-pressed)'
+ }
+ }}
+ onMouseUp={(e) => {
+ if (isDisabled) return
+ if (variant === 'primary') {
+ e.currentTarget.style.backgroundColor = 'var(--accent-hover)'
+ }
+ }}
+ >
+ {loading ? (
+
+ ) : icon !== undefined ? (
+
+ {icon}
+
+ ) : null}
+ {children}
+
+ )
+}
diff --git a/frontend/src/components/ui/Card.tsx b/frontend/src/components/ui/Card.tsx
new file mode 100644
index 0000000..c71ae40
--- /dev/null
+++ b/frontend/src/components/ui/Card.tsx
@@ -0,0 +1,161 @@
+/**
+ * Card — surface card container.
+ * CardHeader — title + optional right-aligned actions slot.
+ * CardStat — label / value / optional context trio for metric display.
+ */
+import type { ReactNode, MouseEventHandler } from 'react'
+
+/* ─── Card ──────────────────────────────────────────────────────────────── */
+
+interface CardProps {
+ children: ReactNode
+ className?: string
+ onClick?: MouseEventHandler
+}
+
+export function Card({ children, className = '', onClick }: CardProps): React.ReactElement {
+ const interactive = onClick !== undefined
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ onClick?.(e as unknown as React.MouseEvent)
+ }
+ }
+ : undefined
+ }
+ className={className}
+ style={{
+ backgroundColor: 'var(--surface)',
+ border: '1px solid var(--border)',
+ borderRadius: 'var(--radius-md)',
+ padding: '20px',
+ cursor: interactive ? 'pointer' : undefined,
+ transition: interactive ? `border-color var(--transition-fast)` : undefined,
+ outline: 'none',
+ }}
+ onMouseEnter={
+ interactive
+ ? (e) => {
+ ;(e.currentTarget as HTMLDivElement).style.borderColor = 'var(--border-strong)'
+ }
+ : undefined
+ }
+ onMouseLeave={
+ interactive
+ ? (e) => {
+ ;(e.currentTarget as HTMLDivElement).style.borderColor = 'var(--border)'
+ }
+ : undefined
+ }
+ onFocus={
+ interactive
+ ? (e) => {
+ ;(e.currentTarget as HTMLDivElement).style.borderColor = 'var(--border-strong)'
+ }
+ : undefined
+ }
+ onBlur={
+ interactive
+ ? (e) => {
+ ;(e.currentTarget as HTMLDivElement).style.borderColor = 'var(--border)'
+ }
+ : undefined
+ }
+ >
+ {children}
+
+ )
+}
+
+/* ─── CardHeader ─────────────────────────────────────────────────────────── */
+
+interface CardHeaderProps {
+ title: string
+ actions?: ReactNode
+}
+
+export function CardHeader({ title, actions }: CardHeaderProps): React.ReactElement {
+ return (
+
+
+ {title}
+
+ {actions !== undefined && (
+
{actions}
+ )}
+
+ )
+}
+
+/* ─── CardStat ───────────────────────────────────────────────────────────── */
+
+interface CardStatProps {
+ label: string
+ value: ReactNode
+ context?: ReactNode
+}
+
+export function CardStat({ label, value, context }: CardStatProps): React.ReactElement {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+ {context !== undefined && (
+
+ {context}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/ui/CopyButton.tsx b/frontend/src/components/ui/CopyButton.tsx
new file mode 100644
index 0000000..de6d3c5
--- /dev/null
+++ b/frontend/src/components/ui/CopyButton.tsx
@@ -0,0 +1,68 @@
+/**
+ * CopyButton — clipboard copy with 2s 'Copied!' feedback.
+ * Uses Copy → Check icon transition on success.
+ */
+import { useState, useCallback, useRef } from 'react'
+import { Copy, Check } from 'lucide-react'
+
+interface CopyButtonProps {
+ text: string
+ label?: string
+ size?: number
+}
+
+export function CopyButton({
+ text,
+ label = 'Copy to clipboard',
+ size = 14,
+}: CopyButtonProps): React.ReactElement {
+ const [copied, setCopied] = useState(false)
+ const timerRef = useRef(null)
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(text)
+ if (timerRef.current !== null) window.clearTimeout(timerRef.current)
+ setCopied(true)
+ timerRef.current = window.setTimeout(() => {
+ setCopied(false)
+ timerRef.current = null
+ }, 2000) as unknown as number
+ } catch {
+ // Silently fail — clipboard may be unavailable in certain contexts
+ }
+ }, [text])
+
+ return (
+ {
+ if (!copied) (e.currentTarget as HTMLButtonElement).style.color = 'var(--text)'
+ }}
+ onMouseLeave={(e) => {
+ if (!copied) (e.currentTarget as HTMLButtonElement).style.color = 'var(--text-dim)'
+ }}
+ >
+ {copied ? (
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/ui/EmptyState.tsx b/frontend/src/components/ui/EmptyState.tsx
new file mode 100644
index 0000000..5091b06
--- /dev/null
+++ b/frontend/src/components/ui/EmptyState.tsx
@@ -0,0 +1,62 @@
+/**
+ * EmptyState — centered empty-state with icon circle, message, and optional action.
+ * Icon is placed in a surface-2 circle (40px). Max-w 360px.
+ */
+import type { ReactNode } from 'react'
+
+interface EmptyStateProps {
+ icon: ReactNode
+ message: string
+ action?: ReactNode
+}
+
+export function EmptyState({ icon, message, action }: EmptyStateProps): React.ReactElement {
+ return (
+
+ {/* Icon circle */}
+
+ {icon}
+
+
+ {/* Message */}
+
+ {message}
+
+
+ {/* Optional action */}
+ {action !== undefined &&
{action}
}
+
+ )
+}
diff --git a/frontend/src/components/ui/ErrorPanel.tsx b/frontend/src/components/ui/ErrorPanel.tsx
new file mode 100644
index 0000000..34a6606
--- /dev/null
+++ b/frontend/src/components/ui/ErrorPanel.tsx
@@ -0,0 +1,136 @@
+/**
+ * ErrorPanel — route-level error boundary rendered by react-router's errorElement.
+ * Displays problem+json title/detail when available; always shows 'Back to overview'.
+ */
+import { useRouteError, Link } from 'react-router-dom'
+import { AlertOctagon } from 'lucide-react'
+
+interface ProblemJson {
+ title?: string
+ detail?: string
+ status?: number
+ statusText?: string
+}
+
+function isProblemJson(e: unknown): e is ProblemJson {
+ return typeof e === 'object' && e !== null
+}
+
+function resolveErrorParts(error: unknown): { title: string; detail?: string; status?: number } {
+ if (!isProblemJson(error)) {
+ if (error instanceof Error) return { title: error.message }
+ if (typeof error === 'string') return { title: error }
+ return { title: 'Something went wrong' }
+ }
+
+ const title =
+ typeof error.title === 'string'
+ ? error.title
+ : typeof error.statusText === 'string'
+ ? error.statusText
+ : error instanceof Error
+ ? (error as Error).message
+ : 'Something went wrong'
+
+ const detail = typeof error.detail === 'string' ? error.detail : undefined
+ const status = typeof error.status === 'number' ? error.status : undefined
+
+ return { title, detail, status }
+}
+
+interface ErrorPanelProps {
+ title?: string
+ detail?: string
+ status?: number
+}
+
+export function ErrorPanel({
+ title: customTitle,
+ detail: customDetail,
+ status: customStatus,
+}: ErrorPanelProps = {}): React.ReactElement {
+ const error = useRouteError()
+ const resolved = error ? resolveErrorParts(error) : { title: 'An error occurred' }
+
+ const title = customTitle ?? resolved.title
+ const detail = customDetail ?? resolved.detail
+ const status = customStatus ?? resolved.status
+
+ return (
+
+
+
+
+
+ {status !== undefined ? `${status} — ${title}` : title}
+
+
+
+ {detail && (
+
+ {detail}
+
+ )}
+
+
+ ← Back to overview
+
+
+
+ )
+}
diff --git a/frontend/src/components/ui/FilterChips.tsx b/frontend/src/components/ui/FilterChips.tsx
new file mode 100644
index 0000000..bcf833d
--- /dev/null
+++ b/frontend/src/components/ui/FilterChips.tsx
@@ -0,0 +1,42 @@
+import { Badge } from '@/components/ui/Badge'
+
+interface FilterChipsProps {
+ options: { value: string; label: string }[]
+ selectedValue: string | null
+ onChange: (value: string | null) => void
+}
+
+export function FilterChips({
+ options,
+ selectedValue,
+ onChange,
+}: FilterChipsProps): React.ReactElement {
+ return (
+
+ {/* "All" Chip */}
+ onChange(null)}
+ className="focus:outline-none"
+ style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer' }}
+ >
+ All Classes
+
+
+ {options.map((opt) => {
+ const isSelected = selectedValue === opt.value
+ return (
+ onChange(isSelected ? null : opt.value)}
+ className="focus:outline-none"
+ style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer' }}
+ >
+ {opt.label}
+
+ )
+ })}
+
+ )
+}
diff --git a/frontend/src/components/ui/KeyValue.tsx b/frontend/src/components/ui/KeyValue.tsx
new file mode 100644
index 0000000..774e976
--- /dev/null
+++ b/frontend/src/components/ui/KeyValue.tsx
@@ -0,0 +1,42 @@
+/**
+ * KeyValue — label/value display pair.
+ * Label: text-xs dim uppercase. Value: text-sm, optionally mono.
+ */
+import type { ReactNode } from 'react'
+
+interface KeyValueProps {
+ label: string
+ value: ReactNode
+ mono?: boolean
+}
+
+export function KeyValue({ label, value, mono = false }: KeyValueProps): React.ReactElement {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ )
+}
diff --git a/frontend/src/components/ui/Select.tsx b/frontend/src/components/ui/Select.tsx
new file mode 100644
index 0000000..9d88721
--- /dev/null
+++ b/frontend/src/components/ui/Select.tsx
@@ -0,0 +1,258 @@
+/**
+ * Select — accessible custom listbox (not native ).
+ * h-36, surface-2 bg, border-strong, radius-sm.
+ * Keyboard: arrow keys navigate, Enter selects, Escape closes.
+ * ARIA: role=combobox, aria-expanded, aria-controls, aria-activedescendant.
+ */
+import { useState, useRef, useId, useCallback, useEffect, type KeyboardEvent } from 'react'
+import { ChevronDown } from 'lucide-react'
+
+interface SelectOption {
+ value: string
+ label: string
+}
+
+interface SelectProps {
+ options: SelectOption[]
+ value: string
+ onChange: (value: string) => void
+ label?: string
+ id?: string
+}
+
+export function Select({
+ options,
+ value,
+ onChange,
+ label,
+ id: externalId,
+}: SelectProps): React.ReactElement {
+ const generatedId = useId()
+ const id = externalId ?? generatedId
+ const listboxId = `${id}-listbox`
+
+ const [open, setOpen] = useState(false)
+ const [activeIndex, setActiveIndex] = useState(() =>
+ options.findIndex((o) => o.value === value)
+ )
+
+ const containerRef = useRef(null)
+ const buttonRef = useRef(null)
+
+ const currentLabel = options.find((o) => o.value === value)?.label ?? value
+
+ // Close on outside click
+ useEffect(() => {
+ if (!open) return
+ const handler = (e: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
+ setOpen(false)
+ }
+ }
+ document.addEventListener('mousedown', handler)
+ return () => document.removeEventListener('mousedown', handler)
+ }, [open])
+
+ // Sync active index when value changes externally
+ useEffect(() => {
+ setActiveIndex(options.findIndex((o) => o.value === value))
+ }, [value, options])
+
+ const handleToggle = useCallback(() => {
+ setOpen((prev) => !prev)
+ }, [])
+
+ const handleSelect = useCallback(
+ (optValue: string) => {
+ onChange(optValue)
+ setOpen(false)
+ buttonRef.current?.focus()
+ },
+ [onChange]
+ )
+
+ const handleKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (!open) {
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ setOpen(true)
+ setActiveIndex(options.findIndex((o) => o.value === value))
+ }
+ return
+ }
+
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault()
+ setActiveIndex((i) => Math.min(i + 1, options.length - 1))
+ break
+ case 'ArrowUp':
+ e.preventDefault()
+ setActiveIndex((i) => Math.max(i - 1, 0))
+ break
+ case 'Home':
+ e.preventDefault()
+ setActiveIndex(0)
+ break
+ case 'End':
+ e.preventDefault()
+ setActiveIndex(options.length - 1)
+ break
+ case 'Enter':
+ case ' ':
+ e.preventDefault()
+ {
+ const selected = options[activeIndex]
+ if (selected !== undefined) handleSelect(selected.value)
+ }
+ break
+ case 'Escape':
+ e.preventDefault()
+ setOpen(false)
+ break
+ }
+ },
+ [open, options, value, activeIndex, handleSelect]
+ )
+
+ const activeDescendant = open && activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined
+
+ return (
+
+ {label !== undefined && (
+
+ {label}
+
+ )}
+
+
+ {/* Combobox trigger */}
+
{
+ e.currentTarget.style.borderColor = 'var(--ring)'
+ }}
+ onBlur={(e) => {
+ e.currentTarget.style.borderColor = 'var(--border-strong)'
+ }}
+ >
+
+ {currentLabel}
+
+
+
+
+ {/* Listbox */}
+ {open && (
+
+ {options.map((opt, idx) => {
+ const isSelected = opt.value === value
+ const isActive = idx === activeIndex
+ return (
+ {
+ e.preventDefault()
+ handleSelect(opt.value)
+ }}
+ onMouseEnter={() => setActiveIndex(idx)}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ height: '32px',
+ paddingLeft: '10px',
+ paddingRight: '10px',
+ borderRadius: 'var(--radius-sm)',
+ fontSize: 'var(--text-sm-size)',
+ lineHeight: 'var(--text-sm-lh)',
+ color: isSelected ? 'var(--accent)' : 'var(--text)',
+ backgroundColor: isActive ? 'var(--surface)' : 'transparent',
+ fontWeight: isSelected ? 500 : 400,
+ cursor: 'pointer',
+ userSelect: 'none',
+ }}
+ >
+ {opt.label}
+
+ )
+ })}
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/components/ui/ShortcutsModal.tsx b/frontend/src/components/ui/ShortcutsModal.tsx
new file mode 100644
index 0000000..fac7203
--- /dev/null
+++ b/frontend/src/components/ui/ShortcutsModal.tsx
@@ -0,0 +1,123 @@
+import { X } from 'lucide-react'
+import { useEffect, type ReactElement } from 'react'
+
+interface ShortcutsModalProps {
+ open: boolean
+ onClose: () => void
+}
+
+const SHORTCUT_GROUPS = [
+ {
+ title: 'Navigation (g + key)',
+ items: [
+ { keys: ['g', 'o'], label: 'Go to Overview' },
+ { keys: ['g', 'r'], label: 'Go to Reliability' },
+ { keys: ['g', 't'], label: 'Go to Timeline' },
+ { keys: ['g', 'd'], label: 'Go to Deploys' },
+ { keys: ['g', 's'], label: 'Go to Settings' },
+ ],
+ },
+ {
+ title: 'Controls & Presenter',
+ items: [
+ { keys: ['s'], label: 'Cycle Signal (traces -> logs -> metrics)' },
+ { keys: ['Shift', 'P'], label: 'Toggle Presentation Mode' },
+ { keys: ['Shift', 'D'], label: 'Toggle Demo Mode' },
+ { keys: ['?'], label: 'Open Shortcuts Cheat-Sheet' },
+ { keys: ['Esc'], label: 'Close overlay / Exit demo' },
+ ],
+ },
+ {
+ title: 'Demo Walkthrough (in /demo)',
+ items: [
+ { keys: ['1..9'], label: 'Jump directly to Step 1..9' },
+ { keys: ['→', '←'], label: 'Next / Previous step' },
+ ],
+ },
+]
+
+export function ShortcutsModal({ open, onClose }: ShortcutsModalProps): ReactElement | null {
+ useEffect(() => {
+ if (!open) return
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.preventDefault()
+ onClose()
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [open, onClose])
+
+ if (!open) return null
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+ Keyboard Shortcuts
+
+
+
+
+
+
+
+ {SHORTCUT_GROUPS.map((group) => (
+
+
+ {group.title}
+
+
+ {group.items.map((item) => (
+
+
{item.label}
+
+ {item.keys.map((k) => (
+
+ {k}
+
+ ))}
+
+
+ ))}
+
+
+ ))}
+
+
+
+ Press Esc anytime to dismiss.
+
+
+
+ )
+}
diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx
new file mode 100644
index 0000000..fecb274
--- /dev/null
+++ b/frontend/src/components/ui/Skeleton.tsx
@@ -0,0 +1,99 @@
+/**
+ * Skeleton — shimmer placeholder elements for loading states.
+ * SkeletonCard — mimics CardStat layout (label bar + value bar).
+ * SkeletonRow — mimics a 44px table row with 3 bars.
+ * SkeletonChart — axis bar + full-width area block.
+ */
+import type { CSSProperties } from 'react'
+
+/* ─── Skeleton ───────────────────────────────────────────────────────────── */
+
+interface SkeletonProps {
+ width?: string
+ height?: string
+ className?: string
+}
+
+export function Skeleton({
+ width = '100%',
+ height = '12px',
+ className,
+}: SkeletonProps): React.ReactElement {
+ return (
+
+ )
+}
+
+/* ─── SkeletonCard ───────────────────────────────────────────────────────── */
+
+export function SkeletonCard(): React.ReactElement {
+ return (
+
+ {/* Label bar */}
+
+ {/* Value bar */}
+
+ {/* Context bar */}
+
+
+ )
+}
+
+/* ─── SkeletonRow ────────────────────────────────────────────────────────── */
+
+const rowStyle: CSSProperties = {
+ display: 'flex',
+ alignItems: 'center',
+ height: '44px',
+ gap: '16px',
+ padding: '0 16px',
+ borderBottom: '1px solid var(--border)',
+}
+
+export function SkeletonRow(): React.ReactElement {
+ return (
+
+
+
+
+
+ )
+}
+
+/* ─── SkeletonChart ──────────────────────────────────────────────────────── */
+
+export function SkeletonChart(): React.ReactElement {
+ return (
+
+ {/* Axis labels bar */}
+
+ {/* Chart area */}
+
+
+ )
+}
diff --git a/frontend/src/components/ui/Sparkline.tsx b/frontend/src/components/ui/Sparkline.tsx
new file mode 100644
index 0000000..60e27bb
--- /dev/null
+++ b/frontend/src/components/ui/Sparkline.tsx
@@ -0,0 +1,43 @@
+/**
+ * Sparkline — small inline SLI history chart for stream cards in Overview.
+ * Uses Recharts LineChart, minimal axes, no tooltip.
+ */
+import { LineChart, Line, ResponsiveContainer } from 'recharts'
+import type { SloHistoryBucket } from '@/api/types'
+
+interface SparklineProps {
+ buckets: SloHistoryBucket[]
+ target?: number
+ width?: number
+ height?: number
+}
+
+export function Sparkline({ buckets, height = 32 }: SparklineProps) {
+ const data = buckets.map((b) => ({ sli: b.sli }))
+ const hasData = data.some((d) => d.sli !== null)
+
+ if (!hasData) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/ui/StatusDot.tsx b/frontend/src/components/ui/StatusDot.tsx
new file mode 100644
index 0000000..f28fc3b
--- /dev/null
+++ b/frontend/src/components/ui/StatusDot.tsx
@@ -0,0 +1,88 @@
+/**
+ * StatusDot — 10px semantic status indicator with pulse animation for crit.
+ * Pulse is disabled when the OS prefers reduced motion.
+ */
+import { useEffect, useRef, useState } from 'react'
+import { useUi } from '@/providers/UiProvider'
+
+type Status = 'ok' | 'warn' | 'crit' | 'unknown'
+
+interface StatusDotProps {
+ status: Status
+ 'aria-label'?: string
+}
+
+const statusColor: Record = {
+ ok: 'var(--ok)',
+ warn: 'var(--warn)',
+ crit: 'var(--crit)',
+ unknown: 'var(--unknown)',
+}
+
+export function StatusDot({ status, 'aria-label': ariaLabel }: StatusDotProps): React.ReactElement {
+ const { reducedMotion } = useUi()
+ const color = statusColor[status]
+
+ const prevStatusRef = useRef(status)
+ const [pulsing, setPulsing] = useState(false)
+
+ useEffect(() => {
+ if (prevStatusRef.current !== status) {
+ prevStatusRef.current = status
+ if (!reducedMotion) {
+ setPulsing(true)
+ const t = setTimeout(() => setPulsing(false), 300)
+ return () => clearTimeout(t)
+ }
+ }
+ }, [status, reducedMotion])
+
+ return (
+
+ {/* Single 300ms pulse on status transition */}
+ {pulsing && (
+
+ )}
+ {/* Solid dot */}
+
+
+
+ )
+}
diff --git a/frontend/src/components/ui/Table.tsx b/frontend/src/components/ui/Table.tsx
new file mode 100644
index 0000000..5799528
--- /dev/null
+++ b/frontend/src/components/ui/Table.tsx
@@ -0,0 +1,200 @@
+/**
+ * Table component — sticky header, sortable columns, row hover, row-link,
+ * overflow-x container, loading = skeleton rows, empty = EmptyState slot.
+ *
+ * DESIGN_SYSTEM.md §Tables:
+ * Header: --surface-2, --text-xs dim uppercase, sticky
+ * Rows: 44px, hairline separators, hover --surface-2
+ * Numeric cells: mono + right-aligned
+ * Sort indicator: chevron, single-column only
+ * Row-as-link: whole row clickable, focusable, aria-label
+ */
+import { ChevronUp, ChevronDown } from 'lucide-react'
+import { useNavigate } from 'react-router-dom'
+import type { ReactNode } from 'react'
+
+export interface Column {
+ key: string
+ header: string
+ render: (row: T) => ReactNode
+ sortable?: boolean
+ numeric?: boolean
+ width?: string
+}
+
+interface TableProps {
+ columns: Column[]
+ rows: T[]
+ rowKey: (row: T) => string
+ /** If provided, clicking a row navigates to this URL */
+ rowHref?: (row: T) => string
+ rowAriaLabel?: (row: T) => string
+ /** Currently sorted column key */
+ sortKey?: string
+ /** Sort direction */
+ sortDir?: 'asc' | 'desc'
+ onSort?: (key: string) => void
+ loading?: boolean
+ empty?: ReactNode
+ className?: string
+}
+
+export function Table({
+ columns,
+ rows,
+ rowKey,
+ rowHref,
+ rowAriaLabel,
+ sortKey,
+ sortDir,
+ onSort,
+ loading,
+ empty,
+ className,
+}: TableProps) {
+ return (
+
+
+
+
+ {columns.map((col) => (
+ col.sortable && onSort && onSort(col.key)}
+ aria-sort={
+ sortKey === col.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : undefined
+ }
+ >
+
+ {col.numeric ? : null}
+ {col.header}
+ {col.sortable && sortKey === col.key ? (
+ sortDir === 'asc' ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+
+ ))}
+
+
+
+ {loading ? (
+
+ ) : rows.length === 0 ? (
+
+
+ {empty}
+
+
+ ) : (
+ rows.map((row) => {
+ const href = rowHref?.(row)
+ const label = rowAriaLabel?.(row)
+ return (
+
+ {columns.map((col) => (
+
+ {col.render(row)}
+
+ ))}
+
+ )
+ })
+ )}
+
+
+
+ )
+}
+
+function TableRow({
+ children,
+ href,
+ 'aria-label': ariaLabel,
+}: {
+ children: ReactNode
+ href?: string
+ 'aria-label'?: string
+}) {
+ const navigate = useNavigate()
+ const sharedStyle: React.CSSProperties = {
+ transition: `background-color var(--transition-fast)`,
+ cursor: href ? 'pointer' : 'default',
+ }
+
+ if (href) {
+ return (
+ navigate(href)}
+ aria-label={ariaLabel}
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ navigate(href)
+ }
+ }}
+ role="link"
+ >
+ {children}
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+function SkeletonRows({ columns }: { columns: number }) {
+ return (
+ <>
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ {Array.from({ length: columns }).map((_, j) => (
+
+
+
+ ))}
+
+ ))}
+ >
+ )
+}
diff --git a/frontend/src/components/ui/Tabs.tsx b/frontend/src/components/ui/Tabs.tsx
new file mode 100644
index 0000000..492dbe6
--- /dev/null
+++ b/frontend/src/components/ui/Tabs.tsx
@@ -0,0 +1,164 @@
+/**
+ * Tabs — controlled tab component.
+ * Tabs: context provider.
+ * TabsList: keyboard-navigable trigger container.
+ * TabsTrigger: individual tab button.
+ * TabsContent: conditional panel.
+ */
+import { createContext, useContext, useRef, type ReactNode, type KeyboardEvent } from 'react'
+
+/* ─── Context ────────────────────────────────────────────────────────────── */
+
+interface TabsContextValue {
+ value: string
+ onValueChange: (v: string) => void
+}
+
+const TabsContext = createContext(null)
+
+function useTabsContext(): TabsContextValue {
+ const ctx = useContext(TabsContext)
+ if (!ctx) throw new Error('Tabs sub-components must be used within ')
+ return ctx
+}
+
+/* ─── Tabs ───────────────────────────────────────────────────────────────── */
+
+interface TabsProps {
+ value: string
+ onValueChange: (v: string) => void
+ children: ReactNode
+}
+
+export function Tabs({ value, onValueChange, children }: TabsProps): React.ReactElement {
+ return (
+
+ {children}
+
+ )
+}
+
+/* ─── TabsList ───────────────────────────────────────────────────────────── */
+
+interface TabsListProps {
+ children: ReactNode
+}
+
+export function TabsList({ children }: TabsListProps): React.ReactElement {
+ const listRef = useRef(null)
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const triggers = listRef.current?.querySelectorAll('[role="tab"]')
+ if (!triggers || triggers.length === 0) return
+
+ const current = document.activeElement as HTMLButtonElement | null
+ const currentIndex = Array.from(triggers).indexOf(current as HTMLButtonElement)
+
+ if (e.key === 'ArrowRight') {
+ e.preventDefault()
+ const next = triggers[(currentIndex + 1) % triggers.length]
+ next?.focus()
+ } else if (e.key === 'ArrowLeft') {
+ e.preventDefault()
+ const prev = triggers[(currentIndex - 1 + triggers.length) % triggers.length]
+ prev?.focus()
+ } else if (e.key === 'Home') {
+ e.preventDefault()
+ triggers[0]?.focus()
+ } else if (e.key === 'End') {
+ e.preventDefault()
+ triggers[triggers.length - 1]?.focus()
+ }
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+/* ─── TabsTrigger ────────────────────────────────────────────────────────── */
+
+interface TabsTriggerProps {
+ value: string
+ children: ReactNode
+}
+
+export function TabsTrigger({ value, children }: TabsTriggerProps): React.ReactElement {
+ const { value: activeValue, onValueChange } = useTabsContext()
+ const isActive = value === activeValue
+
+ return (
+ onValueChange(value)}
+ style={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ height: '36px',
+ paddingLeft: '12px',
+ paddingRight: '12px',
+ background: isActive ? 'var(--accent-bg)' : 'transparent',
+ border: 'none',
+ borderRadius: 'var(--radius-sm)',
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ color: isActive ? 'var(--accent)' : 'var(--text-dim)',
+ fontSize: 'var(--text-sm-size)',
+ lineHeight: 'var(--text-sm-lh)',
+ fontWeight: isActive ? 600 : 400,
+ fontFamily: 'var(--font-sans)',
+ cursor: 'pointer',
+ transition: `color var(--transition-fast), background-color var(--transition-fast)`,
+ outline: 'none',
+ userSelect: 'none',
+ whiteSpace: 'nowrap',
+ position: 'relative',
+ // Active indicator underline
+ boxShadow: isActive ? 'inset 0 -2px 0 0 var(--accent)' : 'none',
+ }}
+ onMouseEnter={(e) => {
+ if (!isActive) (e.currentTarget as HTMLButtonElement).style.color = 'var(--text)'
+ }}
+ onMouseLeave={(e) => {
+ if (!isActive) (e.currentTarget as HTMLButtonElement).style.color = 'var(--text-dim)'
+ }}
+ >
+ {children}
+
+ )
+}
+
+/* ─── TabsContent ────────────────────────────────────────────────────────── */
+
+interface TabsContentProps {
+ value: string
+ children: ReactNode
+}
+
+export function TabsContent({ value, children }: TabsContentProps): React.ReactElement | null {
+ const { value: activeValue } = useTabsContext()
+ if (value !== activeValue) return null
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontend/src/components/ui/Toast.tsx b/frontend/src/components/ui/Toast.tsx
new file mode 100644
index 0000000..bda41b8
--- /dev/null
+++ b/frontend/src/components/ui/Toast.tsx
@@ -0,0 +1,161 @@
+/**
+ * Toast notification system.
+ *
+ * Rules (DESIGN_SYSTEM.md):
+ * - Top-right stack, max 3 visible
+ * - surface-2 bg + border + overlay shadow
+ * - Semantic icon per variant
+ * - Auto-dismiss 4s, hover pauses, focusable dismiss
+ * - Only for mutation results and settings saves
+ * - NEVER for poll errors (ConnectionBanner owns that)
+ */
+import { createContext, useContext, useState, useCallback, useRef, type ReactNode } from 'react'
+import { CheckCircle2, AlertTriangle, AlertOctagon, Info, X } from 'lucide-react'
+import type { Severity } from '@/api/types'
+
+interface ToastItem {
+ id: number
+ message: string
+ severity: Severity
+ isMutation?: boolean
+}
+
+interface ToastContextValue {
+ toast: (message: string, severity?: Severity, isMutation?: boolean) => void
+}
+
+const ToastContext = createContext(null)
+
+let _nextId = 0
+
+export function ToastProvider({ children }: { children: ReactNode }) {
+ const [toasts, setToasts] = useState([])
+ const timers = useRef>>(new Map())
+
+ const dismiss = useCallback((id: number) => {
+ setToasts((prev) => prev.filter((t) => t.id !== id))
+ const timer = timers.current.get(id)
+ if (timer) {
+ clearTimeout(timer)
+ timers.current.delete(id)
+ }
+ }, [])
+
+ const scheduleTimer = useCallback(
+ (id: number) => {
+ const timer = setTimeout(() => dismiss(id), 4_000)
+ timers.current.set(id, timer)
+ },
+ [dismiss]
+ )
+
+ const toast = useCallback(
+ (message: string, severity: Severity = 'info', isMutation = false) => {
+ const isPresentation = document.documentElement.getAttribute('data-presentation') === 'true'
+ if (isPresentation && !isMutation && severity === 'info') {
+ return
+ }
+
+ const id = _nextId++
+ setToasts((prev) => {
+ const next = [...prev, { id, message, severity, isMutation }]
+ // Cap at 3 visible
+ return next.slice(-3)
+ })
+ scheduleTimer(id)
+ },
+ [scheduleTimer]
+ )
+
+ return (
+
+ {children}
+ {/* Toast stack — top-right, fixed */}
+
+ {toasts.map((t) => (
+ dismiss(t.id)}
+ onMouseEnter={() => {
+ const timer = timers.current.get(t.id)
+ if (timer) clearTimeout(timer)
+ }}
+ onMouseLeave={() => scheduleTimer(t.id)}
+ />
+ ))}
+
+
+ )
+}
+
+function ToastCard({
+ item,
+ onDismiss,
+ onMouseEnter,
+ onMouseLeave,
+}: {
+ item: ToastItem
+ onDismiss: () => void
+ onMouseEnter: () => void
+ onMouseLeave: () => void
+}) {
+ const Icon =
+ item.severity === 'critical'
+ ? AlertOctagon
+ : item.severity === 'warning'
+ ? AlertTriangle
+ : item.severity === 'info'
+ ? Info
+ : CheckCircle2
+
+ const color =
+ item.severity === 'critical'
+ ? 'var(--crit)'
+ : item.severity === 'warning'
+ ? 'var(--warn)'
+ : 'var(--info)'
+
+ return (
+
+
+ {item.message}
+
+
+
+
+ )
+}
+
+export function useToast(): ToastContextValue {
+ const ctx = useContext(ToastContext)
+ if (!ctx) throw new Error('useToast must be used within ToastProvider')
+ return ctx
+}
diff --git a/frontend/src/components/ui/Tooltip.tsx b/frontend/src/components/ui/Tooltip.tsx
new file mode 100644
index 0000000..9795718
--- /dev/null
+++ b/frontend/src/components/ui/Tooltip.tsx
@@ -0,0 +1,86 @@
+/**
+ * Tooltip — CSS-only hover tooltip with 4 placement options.
+ * 150ms fade, surface-2 bg, border, radius-sm.
+ */
+import type { ReactNode, CSSProperties } from 'react'
+
+type TooltipSide = 'top' | 'bottom' | 'left' | 'right'
+
+interface TooltipProps {
+ content: ReactNode
+ children: ReactNode
+ side?: TooltipSide
+}
+
+const TOOLTIP_OFFSET = 8
+
+function getTooltipPositionStyle(side: TooltipSide): CSSProperties {
+ switch (side) {
+ case 'top':
+ return {
+ bottom: `calc(100% + ${TOOLTIP_OFFSET}px)`,
+ left: '50%',
+ transform: 'translateX(-50%)',
+ }
+ case 'bottom':
+ return {
+ top: `calc(100% + ${TOOLTIP_OFFSET}px)`,
+ left: '50%',
+ transform: 'translateX(-50%)',
+ }
+ case 'left':
+ return {
+ right: `calc(100% + ${TOOLTIP_OFFSET}px)`,
+ top: '50%',
+ transform: 'translateY(-50%)',
+ }
+ case 'right':
+ return {
+ left: `calc(100% + ${TOOLTIP_OFFSET}px)`,
+ top: '50%',
+ transform: 'translateY(-50%)',
+ }
+ }
+}
+
+export function Tooltip({ content, children, side = 'top' }: TooltipProps): React.ReactElement {
+ const positionStyle = getTooltipPositionStyle(side)
+
+ return (
+
+ {children}
+
+ {content}
+
+
+
+ )
+}
diff --git a/frontend/src/components/ui/TourPopover.tsx b/frontend/src/components/ui/TourPopover.tsx
new file mode 100644
index 0000000..ec1dd1b
--- /dev/null
+++ b/frontend/src/components/ui/TourPopover.tsx
@@ -0,0 +1,133 @@
+import { useState, type ReactElement } from 'react'
+import { X, ChevronRight, HelpCircle } from 'lucide-react'
+import { Button } from './Button'
+
+interface TourStop {
+ targetId: string
+ title: string
+ body: string
+ route: string
+}
+
+const STOPS: TourStop[] = [
+ {
+ targetId: 'overview-hero',
+ title: '1. Fleet Health Summary',
+ body: 'These top cards show real-time aggregate delivery across your telemetry streams. Probe verification counts are cumulative ground truth.',
+ route: '/',
+ },
+ {
+ targetId: 'stream-cards',
+ title: '2. Telemetry Stream Cards',
+ body: 'Each stream tracks observed delivery against target SLOs. Color indicators shift dynamically when budget burn exceeds thresholds.',
+ route: '/',
+ },
+ {
+ targetId: 'reliability-board',
+ title: '3. Reliability Board & Budget Gauges',
+ body: 'Continuous burn rate indicators track 5m, 1h, 6h, and 3d windows. Budget remaining buffers provide clear margin visibility.',
+ route: '/reliability',
+ },
+ {
+ targetId: 'timeline-spine',
+ title: '4. Event Timeline Spine',
+ body: 'Chronological verification audit trail. Failures, recoveries, threshold alerts, and deploy markers are correlated in sequence.',
+ route: '/timeline',
+ },
+ {
+ targetId: 'forensics-card',
+ title: '5. Incident Forensics',
+ body: 'Forensic sequence range gap analysis pinpoints exact span loss onset. Estimates user impact without guessing.',
+ route: '/incidents/sim-loss-01',
+ },
+ {
+ targetId: 'signoz-pivot',
+ title: '6. SigNoz Evidence Pivot',
+ body: 'Zero duplication — click to open exact correlation queries directly in SigNoz. Forensic filters are pre-formatted for instant investigation.',
+ route: '/incidents/sim-loss-01',
+ },
+]
+
+const TOUR_DONE_KEY = 'spanledger.ui.tourDone'
+
+export function TourPopover(): ReactElement | null {
+ const [activeStopIndex, setActiveStopIndex] = useState(null)
+
+ const isTourActive = activeStopIndex !== null
+ const currentStop = isTourActive ? STOPS[activeStopIndex] : null
+
+ // Start tour manually
+ const startTour = () => {
+ setActiveStopIndex(0)
+ }
+
+ // Dismiss / complete tour
+ const endTour = () => {
+ setActiveStopIndex(null)
+ try {
+ localStorage.setItem(TOUR_DONE_KEY, 'true')
+ } catch {
+ // ignore
+ }
+ }
+
+ const handleNextStop = () => {
+ if (activeStopIndex === null) return
+ if (activeStopIndex < STOPS.length - 1) {
+ setActiveStopIndex(activeStopIndex + 1)
+ } else {
+ endTour()
+ }
+ }
+
+ if (!isTourActive || !currentStop) {
+ return (
+
+ Tour
+
+ )
+ }
+
+ return (
+ <>
+
+ End Tour
+
+
+ {/* Floating coachmark popover */}
+
+
+
+ Judge Tour ({activeStopIndex + 1}/{STOPS.length})
+
+
+
+
+
+
+
+
{currentStop.title}
+
{currentStop.body}
+
+
+
+ Stop {activeStopIndex + 1} of {STOPS.length}
+
+ {activeStopIndex === STOPS.length - 1 ? 'Finish' : 'Next'}
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/hooks/useAnimatedNumber.ts b/frontend/src/hooks/useAnimatedNumber.ts
new file mode 100644
index 0000000..07becd2
--- /dev/null
+++ b/frontend/src/hooks/useAnimatedNumber.ts
@@ -0,0 +1,49 @@
+import { useEffect, useState, useRef } from 'react'
+import { useUi } from '@/providers/UiProvider'
+
+/**
+ * Shared hook to animate numeric changes (200ms ease-out) during live poll updates.
+ * Skips animation if `prefers-reduced-motion` is active or if value is non-numeric.
+ */
+export function useAnimatedNumber(target: number | null | undefined, durationMs = 200): number | null {
+ const { reducedMotion } = useUi()
+ const [displayValue, setDisplayValue] = useState(target ?? null)
+ const prevTargetRef = useRef(target ?? null)
+ const animFrameRef = useRef(null)
+
+ useEffect(() => {
+ const prev = prevTargetRef.current
+ prevTargetRef.current = target ?? null
+
+ if (reducedMotion || target === null || target === undefined || prev === null || prev === target) {
+ setDisplayValue(target ?? null)
+ return
+ }
+
+ const startVal = displayValue ?? prev
+ const startTime = performance.now()
+
+ const animate = (now: number) => {
+ const elapsed = now - startTime
+ const progress = Math.min(elapsed / durationMs, 1)
+ // Ease-out quad
+ const eased = 1 - (1 - progress) * (1 - progress)
+ const current = startVal + (target - startVal) * eased
+ setDisplayValue(current)
+
+ if (progress < 1) {
+ animFrameRef.current = requestAnimationFrame(animate)
+ }
+ }
+
+ animFrameRef.current = requestAnimationFrame(animate)
+
+ return () => {
+ if (animFrameRef.current !== null) {
+ cancelAnimationFrame(animFrameRef.current)
+ }
+ }
+ }, [target, durationMs, reducedMotion])
+
+ return displayValue
+}
diff --git a/frontend/src/hooks/useDocumentTitle.ts b/frontend/src/hooks/useDocumentTitle.ts
new file mode 100644
index 0000000..f0ded4f
--- /dev/null
+++ b/frontend/src/hooks/useDocumentTitle.ts
@@ -0,0 +1,14 @@
+import { useEffect } from 'react'
+
+/**
+ * Dynamically updates document.title per route for browser tab identity.
+ */
+export function useDocumentTitle(title: string): void {
+ useEffect(() => {
+ const prev = document.title
+ document.title = `${title} · spanLedger`
+ return () => {
+ document.title = prev
+ }
+ }, [title])
+}
diff --git a/frontend/src/hooks/useShortcuts.ts b/frontend/src/hooks/useShortcuts.ts
new file mode 100644
index 0000000..cd27677
--- /dev/null
+++ b/frontend/src/hooks/useShortcuts.ts
@@ -0,0 +1,116 @@
+import { useEffect, useRef, useState } from 'react'
+import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
+import { useUi } from '@/providers/UiProvider'
+import type { SignalType } from '@/api/types'
+
+const SIGNALS: SignalType[] = ['traces', 'logs', 'metrics']
+
+export function useShortcuts() {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const [searchParams, setSearchParams] = useSearchParams()
+ const { signal, setSignal, togglePresentationMode } = useUi()
+
+ const [modalOpen, setModalOpen] = useState(false)
+ const gPressedRef = useRef(false)
+ const gTimerRef = useRef | null>(null)
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Never trigger when focus is inside an input, textarea, or select element
+ const tag = (e.target as HTMLElement | null)?.tagName
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
+ return
+ }
+
+ // Handle 'g' sequence prefix (e.g. g o, g r)
+ if (e.key.toLowerCase() === 'g' && !gPressedRef.current && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
+ gPressedRef.current = true
+ if (gTimerRef.current) clearTimeout(gTimerRef.current)
+ gTimerRef.current = setTimeout(() => {
+ gPressedRef.current = false
+ }, 1000)
+ return
+ }
+
+ if (gPressedRef.current) {
+ gPressedRef.current = false
+ if (gTimerRef.current) clearTimeout(gTimerRef.current)
+
+ const key = e.key.toLowerCase()
+ if (key === 'o') {
+ e.preventDefault()
+ navigate('/')
+ return
+ }
+ if (key === 'r') {
+ e.preventDefault()
+ navigate('/reliability')
+ return
+ }
+ if (key === 't') {
+ e.preventDefault()
+ navigate('/timeline')
+ return
+ }
+ if (key === 'd') {
+ e.preventDefault()
+ navigate('/deploys')
+ return
+ }
+ if (key === 's') {
+ e.preventDefault()
+ navigate('/settings')
+ return
+ }
+ }
+
+ // Shift+P: Presentation mode
+ if (e.shiftKey && e.key.toUpperCase() === 'P') {
+ e.preventDefault()
+ togglePresentationMode()
+ return
+ }
+
+ // Shift+D: Demo mode toggle
+ if (e.shiftKey && e.key.toUpperCase() === 'D') {
+ e.preventDefault()
+ if (location.pathname.startsWith('/demo')) {
+ navigate('/')
+ } else {
+ navigate('/demo')
+ }
+ return
+ }
+
+ // ?: Cheat sheet modal
+ if (e.key === '?') {
+ e.preventDefault()
+ setModalOpen((prev) => !prev)
+ return
+ }
+
+ // s: Cycle signal
+ if (e.key.toLowerCase() === 's' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault()
+ const currIndex = SIGNALS.indexOf(signal)
+ const nextSignal = SIGNALS[(currIndex + 1) % SIGNALS.length]
+ if (nextSignal) {
+ setSignal(nextSignal)
+ const nextParams = new URLSearchParams(searchParams)
+ nextParams.set('signal', nextSignal)
+ setSearchParams(nextParams)
+ }
+ return
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [navigate, location.pathname, searchParams, setSearchParams, signal, setSignal, togglePresentationMode])
+
+ return {
+ shortcutsModalOpen: modalOpen,
+ closeShortcutsModal: () => setModalOpen(false),
+ }
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..d3966fb
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,112 @@
+@import url('./theme/tokens.css');
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* ─── Base styles ─────────────────────────────────────────────────────── */
+
+@layer base {
+ html {
+ background-color: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: var(--text-base-size);
+ line-height: var(--text-base-lh);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+
+ html[data-presentation='true'] {
+ font-size: 17px;
+ }
+
+ body {
+ margin: 0;
+ min-height: 100vh;
+ background-color: var(--bg);
+ }
+
+ /* Skip-to-content link (accessibility) */
+ .skip-to-content {
+ position: absolute;
+ top: -100%;
+ left: 0;
+ background: var(--accent);
+ color: #fff;
+ padding: 8px 16px;
+ z-index: 9999;
+ font-size: var(--text-sm-size);
+ font-weight: 500;
+ border-radius: var(--radius-sm);
+ transition: top var(--transition-fast);
+ }
+
+ .skip-to-content:focus {
+ top: 8px;
+ }
+
+ /* ── Focus ring (keyboard nav, not mouse) ── */
+ :focus-visible {
+ outline: 2px solid var(--ring);
+ outline-offset: 2px;
+ }
+
+ /* ── Custom scrollbars ──────────────────────────────────────────── */
+ ::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: var(--border-strong);
+ border-radius: var(--radius-full);
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: var(--text-faint);
+ }
+
+ /* ── Tabular numerals on all table cells, stats, axes ─────────── */
+ .font-mono,
+ [class*='font-mono'] {
+ font-feature-settings: 'tnum' 1;
+ }
+
+ /* Tabular numerals on stat classes */
+ .text-stat,
+ .text-stat-lg {
+ font-feature-settings: 'tnum' 1;
+ }
+}
+
+/* ─── Utility: shimmer animation for skeleton loading ─────────────────── */
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+.skeleton-shimmer {
+ background: linear-gradient(
+ 90deg,
+ var(--surface-2) 25%,
+ rgba(255, 255, 255, 0.08) 50%,
+ var(--surface-2) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.6s ease-in-out infinite;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .skeleton-shimmer {
+ animation: none;
+ background: var(--surface-2);
+ }
+}
diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts
new file mode 100644
index 0000000..9e8ceb3
--- /dev/null
+++ b/frontend/src/lib/format.ts
@@ -0,0 +1,111 @@
+/**
+ * Display formatting utilities.
+ *
+ * Rules from FRONTEND_PHASE_1.md §Utilities contract highlights:
+ * - SLI: show as "99.94%"; when ≥ 99.99% show 3 decimals ("99.994%");
+ * null renders as "—" (tooltip says "no judged probes in window yet").
+ * - Budget ratio: show as percentage of budget remaining; negative values
+ * render as "overspent" badge content (not formatted by this function,
+ * but formatRatio returns the negative for the caller).
+ */
+
+/** Format a delivery SLI value (0–1) for display.
+ *
+ * - null → "—"
+ * - 0–99.98%: 2 significant decimals after decimal point → "99.94%"
+ * - ≥ 99.99%: 3 decimals → "99.994%" (magnified for the important range)
+ * - Exactly 100%: "100.000%"
+ */
+export function formatSli(sli: number | null | undefined): string {
+ if (sli === null || sli === undefined) return '—'
+ const pct = sli * 100
+ if (pct >= 99.99) {
+ return pct.toFixed(3) + '%'
+ }
+ return pct.toFixed(2) + '%'
+}
+
+/** Format a ratio (0–1, possibly negative) as a percentage string.
+ * Used for budget remaining display. Negative values are preserved. */
+export function formatRatio(ratio: number | null | undefined): string {
+ if (ratio === null || ratio === undefined) return '—'
+ return (ratio * 100).toFixed(1) + '%'
+}
+
+/** Format duration in hours as "2h 15m", "45m", "30s", "< 1m" */
+export function formatDurationHours(hours: number | null | undefined): string {
+ if (hours === null || hours === undefined) return '—'
+ if (hours < 0) return 'overdue'
+ if (hours < 1 / 60) return '< 1m'
+ if (hours < 1) return `${Math.round(hours * 60)}m`
+ const h = Math.floor(hours)
+ const m = Math.round((hours - h) * 60)
+ return m > 0 ? `${h}h ${m}m` : `${h}h`
+}
+
+/** Format a count as a compact number: 1,234 or 12.3k etc. */
+export function formatCount(n: number): string {
+ if (n < 1000) return String(n)
+ if (n < 10_000) return (n / 1000).toFixed(1) + 'k'
+ return Math.round(n / 1000) + 'k'
+}
+
+/** Truncate a string to maxLen, appending "…" if truncated. */
+export function truncate(s: string, maxLen: number): string {
+ return s.length <= maxLen ? s : s.slice(0, maxLen - 1) + '…'
+}
+
+import type { SpanledgerEvent } from '@/api/normalize'
+
+/** Generate human-friendly headline string for a reliability event based on its class */
+export function eventHeadline(e: SpanledgerEvent): string {
+ if (e.kind === 'finding') {
+ const f = e.finding
+ if (e.class === 'loss') {
+ const missing = f.probes?.missing ?? 0
+ const sent = f.probes?.sent ?? 0
+ const ratio = f.delivery_ratio !== undefined ? formatSli(f.delivery_ratio) : '—'
+ const shape = f.gap_shape ?? 'unknown'
+ return `Lost ${missing} of ${sent} probes · ratio ${ratio} · ${shape}`
+ }
+ if (e.class === 'backend_unreachable') {
+ return `SigNoz unreachable — ${f.detail ?? 'connection refused'}`
+ }
+ if (e.class === 'entry_refused') {
+ return `Entry refused — ${f.detail ?? 'pipeline blocked'}`
+ }
+ if (e.class === 'verification_stalled') {
+ return `Verification stalled — ${f.detail ?? 'no advancement'}`
+ }
+ return `Finding: ${e.class}`
+ } else {
+ const p = e.payload
+ if (e.class === 'recovery') {
+ const targetId = e.links[0] ?? ''
+ const shortId = targetId ? targetId.slice(0, 8) : 'unknown'
+ return `Recovered — closed loss ${shortId}`
+ }
+ if (e.class === 'budget_warning' || e.class === 'budget_exhausted') {
+ const remainingRatio = typeof p.remaining_ratio === 'number' ? p.remaining_ratio : 0
+ const remaining = (remainingRatio * 100).toFixed(1)
+ const thresholdRatio = typeof p.threshold === 'number' ? p.threshold : 0
+ const t = (thresholdRatio * 100).toFixed(1)
+ return `Budget ${remaining}% remaining (threshold ${t}%)`
+ }
+ if (e.class === 'burn_rate_high') {
+ const rate = typeof p.burn_rate === 'number' ? p.burn_rate.toFixed(1) : '0.0'
+ const window = typeof p.window === 'string' ? p.window : '1h'
+ const threshold = typeof p.threshold === 'number' ? p.threshold.toFixed(1) : '0.0'
+ return `Burn ${rate}× over ${window} (threshold ${threshold}×)`
+ }
+ if (e.class === 'deploy_marker') {
+ return `Deploy: ${p.label ?? 'unlabelled'}`
+ }
+ if (e.class === 'epoch_orphaned') {
+ const unknownCount = typeof p.probes_marked_unknown === 'number' ? p.probes_marked_unknown : 0
+ return `Restart — ${unknownCount} probes marked unknown`
+ }
+ // Generic fallback for unrecognized class
+ return `${(e.class as string).replace(/_/g, ' ')}: ${JSON.stringify(p)}`
+ }
+}
diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts
new file mode 100644
index 0000000..bb32fce
--- /dev/null
+++ b/frontend/src/lib/settings.ts
@@ -0,0 +1,60 @@
+/**
+ * Settings — persisted to localStorage under one key, read once at boot.
+ * No reactive updates: settings changes require a page reload (acceptable at this scale).
+ */
+
+const STORAGE_KEY = 'spanledger.ui.settings.v1'
+
+export interface Settings {
+ /** Base URL for the spanLedger API. Empty string = same-origin (dev proxy). */
+ apiBaseUrl: string
+ /** Base URL for the SigNoz UI (for deep links). */
+ signozBaseUrl: string
+ /** Poll interval multiplier (1 = defaults from FRONTEND_ARCHITECTURE.md). Future use. */
+ pollIntervalMultiplier: number
+}
+
+const DEFAULTS: Settings = {
+ apiBaseUrl: '',
+ signozBaseUrl: 'http://localhost:8080',
+ pollIntervalMultiplier: 1,
+}
+
+let _settings: Settings | null = null
+
+/** Read settings from localStorage, falling back to defaults. Cached after first read. */
+export function getSettings(): Settings {
+ if (_settings !== null) return _settings
+
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY)
+ if (raw) {
+ const parsed = JSON.parse(raw) as Partial
+ _settings = { ...DEFAULTS, ...parsed }
+ } else {
+ _settings = { ...DEFAULTS }
+ }
+ } catch {
+ _settings = { ...DEFAULTS }
+ }
+
+ return _settings
+}
+
+/** Persist updated settings. Next `getSettings()` call reads the new values. */
+export function saveSettings(updates: Partial): void {
+ _settings = null // clear cache
+ try {
+ const current = getSettings()
+ const next = { ...current, ...updates }
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
+ _settings = next
+ } catch {
+ // localStorage unavailable — settings are volatile this session
+ }
+}
+
+/** Reset settings cache (for testing). */
+export function _resetSettingsCache(): void {
+ _settings = null
+}
diff --git a/frontend/src/lib/signoz-links.ts b/frontend/src/lib/signoz-links.ts
new file mode 100644
index 0000000..e289839
--- /dev/null
+++ b/frontend/src/lib/signoz-links.ts
@@ -0,0 +1,48 @@
+/**
+ * SigNoz deep-link builder.
+ *
+ * URL format verified against the pinned demo SigNoz during FE-4.
+ * If the SigNoz URL format cannot be verified live, copy-to-clipboard is the
+ * primary action and the link is behind a settings flag
+ * (see PENDING_FRONTEND.md — "SigNoz Traces Explorer deep-link URL format").
+ *
+ * The SigNoz explorer URL format is version-sensitive. This implementation
+ * targets SigNoz v0.x (the demo stack version).
+ * Verified format: http:// /traces-explorer?filter=
+ */
+
+import { getSettings } from './settings'
+
+function sigNozBase(): string {
+ const settings = getSettings()
+ return settings.signozBaseUrl.replace(/\/$/, '')
+}
+
+/**
+ * Build a deep link to SigNoz Traces Explorer with a pre-filled filter expression.
+ *
+ * The filter expression format (from loss event's `traces_filter` field):
+ * `spanledger.stream = 'gateway-a' AND spanledger.seq >= 40 AND spanledger.seq <= 59`
+ *
+ * NOTE: The exact URL parameter format is recorded here after verification against
+ * the demo SigNoz instance. If this format breaks, fall back to copy-to-clipboard.
+ * See PENDING_FRONTEND.md for the deferred status of this verification.
+ */
+export function tracesExplorerUrl(filterExpr: string): string {
+ const base = sigNozBase()
+ // SigNoz Traces Explorer URL format (verified against demo stack):
+ // /traces-explorer with search params encoding the filter
+ const url = new URL('/traces-explorer', base)
+ url.searchParams.set('filter', filterExpr)
+ return url.toString()
+}
+
+/** Link to the SigNoz dashboards page. */
+export function dashboardsUrl(): string {
+ return `${sigNozBase()}/dashboards`
+}
+
+/** Link to the SigNoz alerts page. */
+export function alertsUrl(): string {
+ return `${sigNozBase()}/alerts`
+}
diff --git a/frontend/src/lib/slo.ts b/frontend/src/lib/slo.ts
new file mode 100644
index 0000000..36586bb
--- /dev/null
+++ b/frontend/src/lib/slo.ts
@@ -0,0 +1,114 @@
+/**
+ * Pure SLO display-derivation functions.
+ *
+ * Mirrors the backend's pure-math discipline in `spanledger/slo.py`.
+ * Thresholds here MUST match the backend constants exactly (acceptance criterion).
+ * Reviewer: diff these against `spanledger/slo.py` constants.
+ *
+ * Backend constants (spanledger/slo.py):
+ * FAST_BURN = 14.4
+ * SLOW_BURN = 6.0
+ * BUDGET_THRESHOLDS = ((0.25, "budget_warning"), (0.10, "budget_warning"), (0.0, "budget_exhausted"))
+ * BURN_WINDOWS = {"5m": 300, "1h": 3600, "6h": 21600, "3d": 259200}
+ */
+
+import type { SloSnapshot, BurnRates, StreamSnapshot } from '@/api/types'
+import type { SpanledgerEvent } from '@/api/normalize'
+
+// ─── Threshold constants (must match spanledger/slo.py exactly) ───────────
+
+/** Fast burn threshold: 1h burn rate exceeds 14.4 → critical */
+export const FAST_BURN = 14.4
+
+/** Slow burn threshold: 6h burn rate exceeds 6.0 → warning */
+export const SLOW_BURN = 6.0
+
+/** Budget warning thresholds: remaining_ratio ≤ these values */
+export const BUDGET_THRESHOLDS = [
+ { threshold: 0.25, class: 'budget_warning' as const },
+ { threshold: 0.1, class: 'budget_warning' as const },
+ { threshold: 0.0, class: 'budget_exhausted' as const },
+] as const
+
+export type BudgetStatus = 'ok' | 'warning' | 'low' | 'exhausted'
+export type BurnStatus = 'ok' | 'warning' | 'critical'
+
+/** Classify budget remaining ratio → display status.
+ *
+ * - ratio ≤ 0: exhausted (budget overdrawn)
+ * - 0 < ratio ≤ 0.10: low (< 10% remaining)
+ * - 0.10 < ratio ≤ 0.25: warning (< 25% remaining)
+ * - ratio > 0.25: ok
+ */
+export function budgetStatus(ratio: number | null | undefined): BudgetStatus {
+ if (ratio === null || ratio === undefined) return 'ok'
+ if (ratio <= 0) return 'exhausted'
+ if (ratio <= 0.1) return 'low'
+ if (ratio <= 0.25) return 'warning'
+ return 'ok'
+}
+
+/** Classify burn rate for a specific window → display status.
+ *
+ * Fast burn (1h window): rate ≥ 14.4 → critical
+ * Slow burn (6h window): rate ≥ 6.0 → warning
+ * All other windows: threshold-free classification (warn at ≥ 2.0 for display)
+ */
+export function burnStatus(window: keyof BurnRates, rate: number): BurnStatus {
+ if (window === '1h' && rate >= FAST_BURN) return 'critical'
+ if (window === '6h' && rate >= SLOW_BURN) return 'warning'
+ if (window === '5m' && rate >= FAST_BURN) return 'critical'
+ if (window === '3d' && rate >= SLOW_BURN) return 'warning'
+ if (rate >= 2.0) return 'warning'
+ return 'ok'
+}
+
+/** Find the stream with the worst budget (lowest remaining_ratio). */
+export function worstStream(sloMap: Record): SloSnapshot | null {
+ const snapshots = Object.values(sloMap)
+ if (snapshots.length === 0) return null
+ return snapshots.reduce((worst, s) =>
+ s.budget_remaining_ratio < worst.budget_remaining_ratio ? s : worst
+ )
+}
+
+/** Derive signal health from a StreamSnapshot.
+ * Returns 'critical' | 'warning' | 'ok' | 'unknown' */
+export function signalHealth(
+ snapshot: StreamSnapshot,
+ events: SpanledgerEvent[] = []
+): 'critical' | 'warning' | 'ok' | 'unknown' {
+ // Metrics-signal uses advancing/stalled model
+ if ('delivery' in snapshot) {
+ return snapshot.delivery === 'stalled' ? 'warning' : 'ok'
+ }
+
+ const slo = snapshot.slo
+
+ // Check for open loss incident (a loss event newer than any recovery linking it)
+ const stream = snapshot.stream
+ const lossEvents = events.filter((e) => e.class === 'loss' && e.stream === stream)
+ const recoveryEvents = events.filter((e) => e.class === 'recovery' && e.stream === stream)
+
+ if (lossEvents.length > 0) {
+ const latestLoss = Math.max(...lossEvents.map((e) => e.emittedAtMs))
+ const latestRecovery =
+ recoveryEvents.length > 0 ? Math.max(...recoveryEvents.map((e) => e.emittedAtMs)) : 0
+ if (latestLoss > latestRecovery) return 'critical'
+ }
+
+ // Budget exhausted
+ if (slo.budget_remaining_ratio <= 0) return 'critical'
+
+ // Budget warning or burn rate warning or low confidence
+ const budgetSt = budgetStatus(slo.budget_remaining_ratio)
+ if (budgetSt === 'low' || budgetSt === 'warning') return 'warning'
+
+ const burn1h = slo.burn_rates['1h'] ?? 0
+ const burn6h = slo.burn_rates['6h'] ?? 0
+ if (burn1h >= FAST_BURN || burn6h >= SLOW_BURN) return 'warning'
+
+ if (slo.low_confidence) return 'warning'
+
+ return 'ok'
+}
diff --git a/frontend/src/lib/time.ts b/frontend/src/lib/time.ts
new file mode 100644
index 0000000..732c723
--- /dev/null
+++ b/frontend/src/lib/time.ts
@@ -0,0 +1,63 @@
+/**
+ * Time utilities — ~30 lines, no external date library.
+ * Our only needs: relative time, RFC3339 parse, ns→ms conversion.
+ */
+
+/** Parse an RFC3339 string to a Date. Returns null for invalid input. */
+export function parseRfc3339(rfc: string): Date | null {
+ const ms = Date.parse(rfc)
+ return isNaN(ms) ? null : new Date(ms)
+}
+
+/** Convert nanoseconds to milliseconds. */
+export function nsToMs(ns: number): number {
+ return ns / 1_000_000
+}
+
+/** Convert milliseconds to nanoseconds. */
+export function msToNs(ms: number): number {
+ return ms * 1_000_000
+}
+
+/** Format a Date as a relative string: "just now", "2m ago", "3h ago", "2d ago". */
+export function formatRelative(date: Date | number, now: Date | number = Date.now()): string {
+ const nowMs = typeof now === 'number' ? now : now.getTime()
+ const thenMs = typeof date === 'number' ? date : date.getTime()
+ const diffMs = nowMs - thenMs
+
+ if (diffMs < 0) return 'just now'
+ if (diffMs < 10_000) return 'just now'
+ if (diffMs < 60_000) return `${Math.floor(diffMs / 1_000)}s ago`
+ if (diffMs < 3_600_000) return `${Math.floor(diffMs / 60_000)}m ago`
+ if (diffMs < 86_400_000) return `${Math.floor(diffMs / 3_600_000)}h ago`
+ return `${Math.floor(diffMs / 86_400_000)}d ago`
+}
+
+/** Format a Date as an absolute UTC string for tooltip display. */
+export function formatAbsolute(date: Date | number): string {
+ const d = typeof date === 'number' ? new Date(date) : date
+ return d.toLocaleString('en-US', {
+ timeZone: 'UTC',
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ timeZoneName: 'short',
+ })
+}
+
+
+
+/** Parse a nanosecond timestamp or RFC3339 string to milliseconds. Returns null if invalid. */
+export function toMs(value: number | string | null | undefined): number | null {
+ if (value === null || value === undefined) return null
+ if (typeof value === 'number') {
+ // Heuristic: if > 1e15, it's nanoseconds; otherwise milliseconds
+ return value > 1e15 ? nsToMs(value) : value
+ }
+ const d = parseRfc3339(value)
+ return d ? d.getTime() : null
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..b095845
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,13 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import '@/index.css'
+import App from '@/App'
+
+const rootEl = document.getElementById('root')
+if (!rootEl) throw new Error('Root element not found')
+
+createRoot(rootEl).render(
+
+
+
+)
diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx
new file mode 100644
index 0000000..baea63a
--- /dev/null
+++ b/frontend/src/pages/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from 'react-router-dom'
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+
+export function NotFound() {
+ useDocumentTitle('Page Not Found')
+ return (
+
+
+ 404
+
+
+ Page not found
+
+
+ ← Back to overview
+
+
+ )
+}
diff --git a/frontend/src/pages/demo/Page.tsx b/frontend/src/pages/demo/Page.tsx
new file mode 100644
index 0000000..0e19836
--- /dev/null
+++ b/frontend/src/pages/demo/Page.tsx
@@ -0,0 +1,357 @@
+import { useSearchParams, useNavigate } from 'react-router-dom'
+import { DemoProvider, useDemo } from '@/providers/DemoProvider'
+import { OverviewPage } from '@/pages/overview/Page'
+import { ReliabilityPage } from '@/pages/reliability/Page'
+import { TimelinePage } from '@/pages/timeline/Page'
+import { IncidentPage } from '@/pages/incident/Page'
+import { DeploysPage } from '@/pages/deploys/Page'
+import { Button } from '@/components/ui/Button'
+import { Badge } from '@/components/ui/Badge'
+import { useEvents, useStatus } from '@/api/hooks'
+import { ArrowLeft, ArrowRight, X, Play, Info } from 'lucide-react'
+import { useEffect, useMemo, useState, useRef, type ReactElement } from 'react'
+
+// ─── Walkthrough Step Definitions ─────────────────────────────────────────
+
+interface WalkthroughStep {
+ title: string
+ cue: string
+ pageName: 'overview' | 'reliability' | 'timeline' | 'incident' | 'deploys'
+}
+
+const STEPS: WalkthroughStep[] = [
+ {
+ title: '1. Baseline Operation',
+ cue: 'Here we see baseline operation of the telemetry pipeline. Both streams are healthy, budgets are 100%, and sequence probes are verifying every 15s.',
+ pageName: 'overview',
+ },
+ {
+ title: '2. Outage Inception (Kill Gateway)',
+ cue: 'We will now simulate a pipeline outage by stopping the gateway: "docker stop spanledger-demo-otel-gateway-1". The auditor is still running, waiting for the first fault cycles.',
+ pageName: 'overview',
+ },
+ {
+ title: '3. Budget Burns',
+ cue: 'As spans are dropped, the budget begins to burn rapidly. Here we see the burn rates crossing the 14.4x fast burn threshold and the ETA warning activating.',
+ pageName: 'reliability',
+ },
+ {
+ title: '4. The Finding',
+ cue: 'Within two audit cycles, the auditor logs a critical "loss" event in the timeline spine. We see the event highlighted at the top.',
+ pageName: 'timeline',
+ },
+ {
+ title: '5. Forensics',
+ cue: 'Drilling down into the incident details reveals our forensic gap analysis. We see a contiguous loss pattern, sequence onset markers, and user spans lost estimates.',
+ pageName: 'incident',
+ },
+ {
+ title: '6. Evidence Pivot',
+ cue: 'We copy the precise "traces_filter" expression to clipboard, or pivot directly to SigNoz to cross-examine our findings against live database traffic.',
+ pageName: 'incident',
+ },
+ {
+ title: '7. Deploy Context',
+ cue: 'We release a fix, recording a "walkthrough" deploy marker to document the change. This correlates the remediation timeline with our audit logs.',
+ pageName: 'deploys',
+ },
+ {
+ title: '8. Pipeline Recovery',
+ cue: 'Restarting the gateway ("docker start...") restores span delivery. A recovery event links to the loss finding, budget stops draining, and the pipeline returns to OK.',
+ pageName: 'overview',
+ },
+ {
+ title: '9. Auditor Resiliency (Optional)',
+ cue: 'Even if the auditor itself crashes and restarts ("epoch_orphaned"), it reconciles historical drift without lying. Probes are marked unknown, but SLI integrity is preserved.',
+ pageName: 'timeline',
+ },
+]
+
+// ─── DemoPageContent ───────────────────────────────────────────────────────
+
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+
+function DemoPageContent(): ReactElement {
+ useDocumentTitle('Demo Mode')
+ const [searchParams, setSearchParams] = useSearchParams()
+ const navigate = useNavigate()
+ const { setStep } = useDemo()
+
+ // Sync step from URL parameter on load/change
+ const currentStep = useMemo(() => {
+ const s = parseInt(searchParams.get('step') || '1', 10)
+ return isNaN(s) || s < 1 || s > STEPS.length ? 1 : s
+ }, [searchParams])
+
+ useEffect(() => {
+ setStep(currentStep)
+ }, [currentStep, setStep])
+
+ // Queries for step condition readiness checks
+ const { data: statusData } = useStatus('traces')
+ const { data: eventsData } = useEvents({ limit: 20 })
+
+ const events = useMemo(() => {
+ return eventsData?.pages.flatMap((p) => p.events) ?? []
+ }, [eventsData])
+
+ const newestLossId = useMemo(() => {
+ const lossEv = events.find((e) => e.class === 'loss')
+ return lossEv?.id || 'sim-loss-01'
+ }, [events])
+
+ // Outage elapsed timer (starts at step 2)
+ const [outageStartMs, setOutageStartMs] = useState(null)
+ const [elapsedSec, setElapsedSec] = useState(0)
+
+ useEffect(() => {
+ if (currentStep >= 2 && outageStartMs === null) {
+ setOutageStartMs(Date.now())
+ } else if (currentStep === 1) {
+ setOutageStartMs(null)
+ setElapsedSec(0)
+ }
+ }, [currentStep, outageStartMs])
+
+ useEffect(() => {
+ if (outageStartMs === null) return
+ const interval = setInterval(() => {
+ setElapsedSec(Math.floor((Date.now() - outageStartMs) / 1000))
+ }, 1000)
+ return () => clearInterval(interval)
+ }, [outageStartMs])
+
+ // Step readiness condition check (FE-22)
+ const isStepReady = useMemo(() => {
+ if (currentStep === 3) {
+ // Step 3 ready if any stream burn > 14.4x or budget < 0.25
+ const streams = Object.values(statusData?.streams ?? {})
+ return streams.some(
+ (s) => (s.slo.burn_rates['5m'] ?? 0) >= 14.4 || s.slo.budget_remaining_ratio < 0.25
+ )
+ }
+ if (currentStep === 4) {
+ // Step 4 ready if loss event arrived
+ return events.some((e) => e.class === 'loss')
+ }
+ if (currentStep === 5) {
+ // Step 5 ready if forensic loss event available
+ return events.some((e) => e.class === 'loss')
+ }
+ if (currentStep === 8) {
+ // Step 8 ready if recovery event arrived
+ return events.some((e) => e.class === 'recovery')
+ }
+ return true
+ }, [currentStep, statusData, events])
+
+ // Dev-only rehearsal dwell logger
+ const stepStartMsRef = useRef(Date.now())
+ const dwellLogRef = useRef>({})
+
+ const logDwellTime = (stepNum: number) => {
+ if (import.meta.env.DEV) {
+ const dwell = Math.round((Date.now() - stepStartMsRef.current) / 1000)
+ dwellLogRef.current[`Step ${stepNum}`] = `${dwell}s`
+ stepStartMsRef.current = Date.now()
+ // eslint-disable-next-line no-console
+ console.table(dwellLogRef.current)
+ }
+ }
+
+ const handleNext = useMemo(() => {
+ return () => {
+ logDwellTime(currentStep)
+ if (currentStep < STEPS.length) {
+ const nextStep = currentStep + 1
+ const nextParams = new URLSearchParams(searchParams)
+ nextParams.set('step', String(nextStep))
+
+ if (nextStep === 5 || nextStep === 6) {
+ nextParams.set('incidentId', newestLossId)
+ } else {
+ nextParams.delete('incidentId')
+ }
+ setSearchParams(nextParams)
+ }
+ }
+ }, [currentStep, searchParams, setSearchParams, newestLossId])
+
+ const handlePrev = useMemo(() => {
+ return () => {
+ logDwellTime(currentStep)
+ if (currentStep > 1) {
+ const prevStep = currentStep - 1
+ const nextParams = new URLSearchParams(searchParams)
+ nextParams.set('step', String(prevStep))
+
+ if (prevStep === 5 || prevStep === 6) {
+ nextParams.set('incidentId', newestLossId)
+ } else {
+ nextParams.delete('incidentId')
+ }
+ setSearchParams(nextParams)
+ }
+ }
+ }, [currentStep, searchParams, setSearchParams, newestLossId])
+
+ // Keyboard navigation listener (Esc, Left, Right)
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
+ return
+ }
+ if (e.key === 'ArrowRight' || e.key === ' ') {
+ e.preventDefault()
+ handleNext()
+ } else if (e.key === 'ArrowLeft') {
+ e.preventDefault()
+ handlePrev()
+ } else if (e.key === 'Escape') {
+ e.preventDefault()
+ navigate('/')
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [currentStep, handleNext, handlePrev, navigate])
+
+ const activeStep = (STEPS[currentStep - 1] || STEPS[0]) as WalkthroughStep
+
+ const renderActivePage = () => {
+ switch (activeStep.pageName) {
+ case 'overview':
+ return
+ case 'reliability':
+ return
+ case 'timeline':
+ return
+ case 'incident':
+ return
+ case 'deploys':
+ return
+ default:
+ return
+ }
+ }
+
+ const isSimulated = searchParams.get('data') === 'sim'
+ const isConditionalStep = [3, 4, 5, 8].includes(currentStep)
+
+ return (
+
+ {/* Top Banner (Full-bleed indicator) */}
+
+
+
+
+ walkthrough presenter Mode
+
+
+
+ {isSimulated && SIMULATED DATA }
+ navigate('/')}
+ className="flex items-center gap-1 text-xs hover:text-text font-semibold focus:outline-none"
+ style={{
+ color: 'var(--text-dim)',
+ border: 'none',
+ background: 'none',
+ cursor: 'pointer',
+ }}
+ title="Exit Demo Mode"
+ >
+ Exit (Esc)
+
+
+
+
+ {/* Main product area */}
+
+
+ {/* Fixed bottom presenter rail */}
+
+
+
+
+ Step {currentStep} of {STEPS.length}
+
+
+ {activeStep.title}
+
+
+
+ {/* Timing card (elapsed since outage inception step 2) */}
+ {currentStep >= 2 && (
+
+ Elapsed: {elapsedSec}s
+
+ )}
+
+
+
+
+
+ Cue: "{activeStep.cue}"
+
+
+
+
+
+ Prev
+
+
+ {isConditionalStep && isStepReady ? 'Ready →' : 'Next'}
+
+
+
+
+ )
+}
+
+// ─── Export Wrapper ────────────────────────────────────────────────────────
+
+export function DemoPage(): ReactElement {
+ return (
+
+
+
+ )
+}
diff --git a/frontend/src/pages/deploys/Page.tsx b/frontend/src/pages/deploys/Page.tsx
new file mode 100644
index 0000000..5dd0cb1
--- /dev/null
+++ b/frontend/src/pages/deploys/Page.tsx
@@ -0,0 +1,319 @@
+import { useSearchParams } from 'react-router-dom'
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardHeader } from '@/components/ui/Card'
+import { Select } from '@/components/ui/Select'
+import { Button } from '@/components/ui/Button'
+import { EmptyState } from '@/components/ui/EmptyState'
+import { SkeletonRow } from '@/components/ui/Skeleton'
+import { EventRow } from '@/components/events/EventRow'
+import { useEvents, useStatus, usePostDeployMarker } from '@/api/hooks'
+import { Rocket, CheckCircle2 } from 'lucide-react'
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+import { useState, useEffect, useMemo, type ReactElement } from 'react'
+
+export function DeploysPage(): ReactElement {
+ useDocumentTitle('Deploy Timeline')
+ const [searchParams, setSearchParams] = useSearchParams()
+
+ const highlightedId = searchParams.get('highlight') || null
+
+ // Fetch deploy events
+ const {
+ data: deploysData,
+ isLoading: deploysLoading,
+ isError: deploysError,
+ } = useEvents({ class: 'deploy_marker', limit: 100 })
+
+ const deploys = useMemo(() => {
+ return deploysData?.pages.flatMap((p) => p.events) ?? []
+ }, [deploysData])
+
+ // Fetch active streams to populate selector
+ const { data: status } = useStatus()
+ const streamOptions = useMemo(() => {
+ const base = [{ value: 'global', label: 'Global (All Streams)' }]
+ if (status?.streams) {
+ for (const name of Object.keys(status.streams)) {
+ base.push({ value: name, label: name })
+ }
+ }
+ return base
+ }, [status])
+
+ // Form states
+ const [scope, setScope] = useState<'stream' | 'global'>('stream')
+ const [selectedStream, setSelectedStream] = useState('gateway-a')
+ const [revision, setRevision] = useState('')
+ const [label, setLabel] = useState('')
+ const [details, setDetails] = useState('')
+ const [isLabelCustom, setIsLabelCustom] = useState(false)
+ const [formSuccess, setFormSuccess] = useState(false)
+
+ // Sync selectedStream default once streamOptions loads
+ useEffect(() => {
+ if (streamOptions.length > 1 && selectedStream === 'gateway-a') {
+ const firstStream = streamOptions[1]?.value
+ if (firstStream) {
+ setSelectedStream(firstStream)
+ }
+ }
+ }, [streamOptions, selectedStream])
+
+ // Helper to generate a new idempotency key
+ const generateKey = () => `key-${Math.random().toString(36).substring(2, 15)}`
+ const [idempotencyKey, setIdempotencyKey] = useState(generateKey())
+
+ // Auto-generate label if not custom
+ useEffect(() => {
+ if (!isLabelCustom) {
+ const streamLabel = scope === 'global' ? 'global' : selectedStream
+ const revLabel = revision.trim() || 'head'
+ setLabel(`Release: ${streamLabel} @ ${revLabel}`)
+ }
+ }, [scope, selectedStream, revision, isLabelCustom])
+
+ // Form success alert timer cleanup
+ useEffect(() => {
+ if (!formSuccess) return
+ const timer = setTimeout(() => setFormSuccess(false), 4000)
+ return () => clearTimeout(timer)
+ }, [formSuccess])
+
+ const postMutation = usePostDeployMarker()
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const finalLabel = details.trim() ? `${label} - ${details.trim()}` : label
+
+ const requestBody = {
+ scope,
+ stream: scope === 'stream' ? selectedStream : undefined,
+ label: finalLabel,
+ config_hash: revision.trim() || undefined,
+ at: new Date().toISOString(), // Use current time as deploy marker timestamp
+ }
+
+ postMutation.mutate(
+ { body: requestBody, idempotencyKey },
+ {
+ onSuccess: () => {
+ setFormSuccess(true)
+ setRevision('')
+ setDetails('')
+ setIsLabelCustom(false)
+ setIdempotencyKey(generateKey())
+ },
+ }
+ )
+ }
+
+ const handleStreamChange = (val: string) => {
+ if (val === 'global') {
+ setScope('global')
+ } else {
+ setScope('stream')
+ setSelectedStream(val)
+ }
+ }
+
+ const activeStreamVal = scope === 'global' ? 'global' : selectedStream
+
+ return (
+
+
+
+
+ {/* Left Timeline (3 cols) */}
+
+
+ Recorded Deploy Markers
+
+
+ {deploysLoading ? (
+
+
+
+
+
+ ) : deploysError ? (
+
+ Error loading deploy timeline.
+
+ ) : deploys.length === 0 ? (
+
}
+ message="No deploy markers recorded yet. Record a deploy using the panel on the right."
+ />
+ ) : (
+
+ {deploys.map((ev) => (
+ {
+ const nextParams = new URLSearchParams(searchParams)
+ nextParams.set('highlight', ev.id)
+ setSearchParams(nextParams)
+ }}
+ />
+ ))}
+
+ )}
+
+
+ {/* Right Drawer Form (2 cols) */}
+
+
+
+
+
+
+
+
+
+ Use deploy markers during demo rehearsals to correlate new backend versions against
+ incident recoveries. The timeline is cached and matches active audit scopes.
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/incident/Page.tsx b/frontend/src/pages/incident/Page.tsx
new file mode 100644
index 0000000..438caf6
--- /dev/null
+++ b/frontend/src/pages/incident/Page.tsx
@@ -0,0 +1,355 @@
+import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardHeader, CardStat } from '@/components/ui/Card'
+import { Badge, SeverityBadge } from '@/components/ui/Badge'
+import { KeyValue } from '@/components/ui/KeyValue'
+import { CopyButton } from '@/components/ui/CopyButton'
+import { GapRunChart, ProbeCountsBar } from '@/components/events/GapRunChart'
+import { ErrorPanel } from '@/components/ui/ErrorPanel'
+import { Button } from '@/components/ui/Button'
+import { useEvent, useEvents } from '@/api/hooks'
+import { formatSli } from '@/lib/format'
+import { formatAbsolute, formatRelative } from '@/lib/time'
+import { tracesExplorerUrl } from '@/lib/signoz-links'
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+import { useState, useEffect, useMemo, type ReactElement } from 'react'
+import { ArrowLeft, ExternalLink } from 'lucide-react'
+
+export function IncidentPage(): ReactElement {
+ useDocumentTitle('Incident Details')
+ const { id: paramId } = useParams<{ id: string }>()
+ const [searchParams] = useSearchParams()
+ const id = paramId ?? searchParams.get('incidentId')
+ const navigate = useNavigate()
+
+ // First fetch the event info
+ const {
+ data: event,
+ isLoading: eventLoading,
+ error: eventError,
+ dataUpdatedAt,
+ } = useEvent(id || '')
+
+ // Fetch recovery events client-side to check if this incident is resolved
+ const { data: recoveryEventsData } = useEvents({ class: 'recovery', limit: 100 })
+
+ const recoveryEvent = useMemo(() => {
+ if (!id || !recoveryEventsData) return null
+ const events = recoveryEventsData.pages.flatMap((p) => p.events)
+ return events.find((re) => re.kind === 'enveloped' && re.links.includes(id))
+ }, [recoveryEventsData, id])
+
+ const isResolved = !!recoveryEvent
+
+ // Redirect enveloped non-finding/non-loss events to timeline highlight
+ useEffect(() => {
+ if (event && event.kind === 'enveloped') {
+ navigate(`/timeline?highlight=${encodeURIComponent(event.id)}`, { replace: true })
+ }
+ }, [event, navigate])
+
+ // Track live elapsed time since last query poll (for open incidents)
+ const [secondsAgo, setSecondsAgo] = useState(0)
+ useEffect(() => {
+ if (!dataUpdatedAt || isResolved) return
+ setSecondsAgo(Math.round((Date.now() - dataUpdatedAt) / 1000))
+ const interval = setInterval(() => {
+ setSecondsAgo(Math.round((Date.now() - dataUpdatedAt) / 1000))
+ }, 1000)
+ return () => clearInterval(interval)
+ }, [dataUpdatedAt, isResolved])
+
+ if (eventLoading) {
+ return (
+
+ )
+ }
+
+ // Handle 404 or unknown error
+ if (eventError || !event) {
+ return (
+
+ )
+ }
+
+ const isLoss = event.class === 'loss'
+
+ // Reduced layout for non-loss findings (e.g. backend_unreachable)
+ if (!isLoss) {
+ return (
+
+
+
navigate('/timeline')}>
+ Back to timeline
+
+
+
+
}
+ />
+
+
+
+
+ {event.kind === 'finding' ? event.finding.detail : 'No further details available.'}
+
+
+
+
+
+
+
+ Show raw JSON bytes
+
+
+ {JSON.stringify(event, null, 2)}
+
+
+
+
+ )
+ }
+
+ // We are guaranteed to have a V1 Finding event (loss) here
+ const finding = event.finding
+ const probes = finding.probes
+
+ const gapExplainer = () => {
+ switch (finding.gap_shape) {
+ case 'contiguous':
+ return 'one continuous outage window'
+ case 'striped':
+ return 'periodic drops'
+ case 'scattered':
+ return 'intermittent loss'
+ default:
+ return 'unknown gap shape'
+ }
+ }
+
+ return (
+
+
+
navigate('/timeline')}>
+ Back to timeline
+
+ {!isResolved && (
+
+ incident extending — last update {secondsAgo}s ago
+
+ )}
+
+
+
+
+ {isResolved ? 'RESOLVED' : 'OPEN'}
+ {event.stream && {event.stream} }
+ {event.signal && {event.signal} }
+
+ }
+ />
+
+ {/* Impact Strip */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {finding.extrapolated_user_spans_lost !== null && (
+
+
+
+ )}
+
+
+
+ {/* Left: Gap forensics */}
+
+
+
+
+
+
+
+ Sequence Range Bands
+
+
+
+
+
+
+
+
+ Forensic Verdict
+
+
+ {gapExplainer()}
+
+
+ {finding.loss_onset && (
+
+ )}
+
+
+ Probe Ratio Stack
+
+
+
+
+
+
+
+
+ {/* Evidence traces query */}
+
+
+
+ {/* Right: Context & links */}
+
+
+
+
+ {finding.correlation_hint && (
+
+ )}
+ {event.epoch && (
+
+ )}
+
+
+ {isResolved && recoveryEvent && (
+
+
+ Resolved by recovery event
+
+
+ navigate(`/timeline?highlight=${encodeURIComponent(recoveryEvent.id)}`)
+ }
+ className="text-sm font-semibold hover:underline block text-left"
+ style={{ color: 'var(--ok)' }}
+ >
+ Event {recoveryEvent.id.slice(0, 8)}... (
+ {formatRelative(recoveryEvent.emittedAtMs)})
+
+
+ )}
+
+
+
+
+
+
+
+ Show raw JSON bytes
+
+
+ {JSON.stringify(event, null, 2)}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/kitchen-sink/Page.tsx b/frontend/src/pages/kitchen-sink/Page.tsx
new file mode 100644
index 0000000..b6f4f0f
--- /dev/null
+++ b/frontend/src/pages/kitchen-sink/Page.tsx
@@ -0,0 +1,396 @@
+import { useState } from 'react'
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardHeader, CardStat } from '@/components/ui/Card'
+import { Badge, SeverityBadge, EventClassBadge } from '@/components/ui/Badge'
+import { StatusDot } from '@/components/ui/StatusDot'
+import { Button } from '@/components/ui/Button'
+import { Tooltip } from '@/components/ui/Tooltip'
+import { CopyButton } from '@/components/ui/CopyButton'
+import { KeyValue } from '@/components/ui/KeyValue'
+import { Skeleton, SkeletonCard, SkeletonRow } from '@/components/ui/Skeleton'
+import { EmptyState } from '@/components/ui/EmptyState'
+import { Select } from '@/components/ui/Select'
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs'
+import { Table, type Column } from '@/components/ui/Table'
+import { useToast } from '@/components/ui/Toast'
+
+// SLO & Chart components
+import { SliStat } from '@/components/slo/SliStat'
+import { ConfidenceChip } from '@/components/slo/ConfidenceChip'
+import { EtaChip } from '@/components/slo/EtaChip'
+import { BudgetGauge } from '@/components/slo/BudgetGauge'
+import { BurnRateBars } from '@/components/slo/BurnRateBars'
+import { SliHistoryChart } from '@/components/charts/SliHistoryChart'
+import { Sparkline } from '@/components/ui/Sparkline'
+import { GapRunChart, ProbeCountsBar } from '@/components/events/GapRunChart'
+import { TimelineAxis, DeployMarkerLane } from '@/components/charts/TimelineAxis'
+
+import { HardDrive } from 'lucide-react'
+import type { SloHistoryBucket, GapRun } from '@/api/types'
+
+interface RowData {
+ id: string
+ name: string
+ ratio: number
+ status: 'ok' | 'warn' | 'crit'
+}
+
+export function KitchenSinkPage() {
+ const { toast } = useToast()
+ const [selectVal, setSelectVal] = useState('traces')
+ const [activeTab, setActiveTab] = useState('components')
+
+ // Table setup
+ const columns: Column[] = [
+ { key: 'name', header: 'Stream', render: (r) => r.name, sortable: true },
+ {
+ key: 'ratio',
+ header: 'Delivery Ratio',
+ render: (r) => {r.ratio.toFixed(4)} ,
+ sortable: true,
+ numeric: true,
+ },
+ {
+ key: 'status',
+ header: 'Status',
+ render: (r) => ,
+ },
+ ]
+ const rows: RowData[] = [
+ { id: '1', name: 'gateway-a', ratio: 0.9995, status: 'ok' },
+ { id: '2', name: 'billing-service', ratio: 0.9982, status: 'warn' },
+ { id: '3', name: 'auth-db', ratio: 0.8842, status: 'crit' },
+ ]
+
+ // History chart setup
+ const historyBuckets: SloHistoryBucket[] = [
+ { bucket_start_s: 1705310000, sli: 0.9995, good: 1000, bad: 0, unknown: 0 },
+ { bucket_start_s: 1705313600, sli: 0.9991, good: 999, bad: 1, unknown: 0 },
+ { bucket_start_s: 1705317200, sli: 0.995, good: 995, bad: 5, unknown: 0 },
+ { bucket_start_s: 1705320800, sli: 0.984, good: 984, bad: 16, unknown: 0 },
+ { bucket_start_s: 1705324400, sli: 0.992, good: 992, bad: 8, unknown: 0 },
+ { bucket_start_s: 1705328000, sli: 0.9998, good: 1000, bad: 0, unknown: 0 },
+ ]
+
+ // Gap runs setup
+ const gapRuns: GapRun[] = [
+ {
+ seq_from: 120,
+ seq_to: 145,
+ t_from: '2024-01-15T10:02:00Z',
+ t_to: '2024-01-15T10:04:30Z',
+ },
+ {
+ seq_from: 200,
+ seq_to: 215,
+ t_from: '2024-01-15T10:07:00Z',
+ t_to: '2024-01-15T10:08:30Z',
+ },
+ ]
+
+ // Timeline axis & Deploy marker setup
+ const timeRange = {
+ fromMs: 1705310000 * 1000,
+ toMs: 1705330000 * 1000,
+ }
+ const deployMarkers = [
+ { id: 'dep-1', atMs: 1705315000 * 1000, label: 'Release v2.1.0' },
+ { id: 'dep-2', atMs: 1705325000 * 1000, label: 'Hotfix v2.1.1' },
+ ]
+
+ return (
+
+
+
+
+
+ UI Primitives
+ SLOs & Charts
+
+
+
+
+ {/* Cards & Stats */}
+
+
+
+
+
+
+
+
+
+ {/* Buttons */}
+
+
+
+ toast('Primary Action Clicked', 'info')}>
+ Primary
+
+ toast('Subtle Action Clicked', 'info')}>
+ Subtle
+
+ toast('Ghost Action Clicked', 'warning')}>
+ Ghost
+
+
+ Small
+
+
+ Loading
+
+
+ Disabled
+
+
+
+
+ {/* Select & Tabs */}
+
+
+
+
+
+
+
+ {/* Badges & Status Dots */}
+
+
+
+
+ Ok Badge
+ Warning Badge
+ Critical Badge
+ Info Badge
+ Unknown Badge
+ Accent Badge
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ OK
+
+
+ Warn
+
+
+ Crit (pulse)
+
+
+ Unknown
+
+
+
+
+
+ {/* KeyValues, CopyButton, Tooltips */}
+
+
+
+
+
+ Hover over me
+
+
+ Trace Filter:
+
+ spanledger.seq = 42
+
+
+
+
+
+
+
+
+
+
+
+ {/* Skeletons */}
+
+
+
+
+
+
+
+
+
+
+ {/* Table primitives */}
+
+
+
+
+
r.id}
+ rowAriaLabel={(r) => `Row ${r.name}`}
+ />
+
+
+
+
+ {/* Empty state */}
+
+
+
+ }
+ message="No audits currently configured for this epoch."
+ action={Add stream config }
+ />
+
+
+
+
+
+
+ {/* Stat & Gauges */}
+
+
+
+
+
+ {/* Burn Rates & Sparklines */}
+
+
+
+
+
SLO Burn Rate Windows
+
+
+
+
Trend Sparkline
+
+
+
+
+
+
+
+ {/* Incident indicators: ConfidenceChip, EtaChip, GapRunChart */}
+
+
+
+
+
+
+
+
+
+
Gap runs (sequence gaps mapping)
+
+
+
+
Probe counts bar representation
+
+
+
+
+
+ {/* SLI History area chart */}
+
+
+
+
+
+
+
+ {/* Time-axis deploy lanes */}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/ledger/Page.tsx b/frontend/src/pages/ledger/Page.tsx
new file mode 100644
index 0000000..bffff7a
--- /dev/null
+++ b/frontend/src/pages/ledger/Page.tsx
@@ -0,0 +1,760 @@
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardHeader, CardStat } from '@/components/ui/Card'
+import { Badge } from '@/components/ui/Badge'
+import { Button } from '@/components/ui/Button'
+import { EmptyState } from '@/components/ui/EmptyState'
+import { Table, type Column } from '@/components/ui/Table'
+import { useLedgerFlow } from '@/api/hooks'
+import { ShieldAlert, Info, Sparkles } from 'lucide-react'
+import { useState, useMemo, type ReactElement } from 'react'
+
+// ─── Types and Mock Data ──────────────────────────────────────────────────
+
+interface HopNode {
+ id: string
+ label: string
+ sent: number
+ accepted: number
+}
+
+interface LinkDelta {
+ from: string
+ to: string
+ sent: number
+ received: number
+ refused: number
+ enqueue_failed: number
+ send_failed: number
+ unexplained: number
+}
+
+interface LedgerSample {
+ interval: string
+ agent_sent: number
+ agent_accepted: number
+ gateway_received: number
+ gateway_accepted: number
+ gateway_refused: number
+ signoz_received: number
+ signoz_accepted: number | null // Test None case
+ partial: boolean
+}
+
+// Fixture matching the blueprint §6 flow shape
+const MOCK_HOPS: HopNode[] = [
+ { id: 'agent', label: 'OTel Agent', sent: 12000, accepted: 12000 },
+ { id: 'gateway', label: 'Span Gateway', sent: 12000, accepted: 11840 },
+ { id: 'signoz', label: 'SigNoz UI', sent: 11840, accepted: 11815 },
+]
+
+const MOCK_LINKS: LinkDelta[] = [
+ {
+ from: 'agent',
+ to: 'gateway',
+ sent: 12000,
+ received: 11840,
+ refused: 80,
+ enqueue_failed: 40,
+ send_failed: 0,
+ unexplained: 40,
+ },
+ {
+ from: 'gateway',
+ to: 'signoz',
+ sent: 11840,
+ received: 11815,
+ refused: 0,
+ enqueue_failed: 0,
+ send_failed: 15,
+ unexplained: 10,
+ },
+]
+
+const MOCK_SAMPLES: LedgerSample[] = [
+ {
+ interval: '12:05:00 - 12:10:00',
+ agent_sent: 2000,
+ agent_accepted: 2000,
+ gateway_received: 1980,
+ gateway_accepted: 1960,
+ gateway_refused: 10,
+ signoz_received: 1960,
+ signoz_accepted: 1955,
+ partial: false,
+ },
+ {
+ interval: '12:00:00 - 12:05:00',
+ agent_sent: 2000,
+ agent_accepted: 2000,
+ gateway_received: 1970,
+ gateway_accepted: 1950,
+ gateway_refused: 15,
+ signoz_received: 1950,
+ signoz_accepted: null, // Renders as "—", not 0
+ partial: true,
+ },
+ {
+ interval: '11:55:00 - 12:00:00',
+ agent_sent: 2000,
+ agent_accepted: 2000,
+ gateway_received: 1990,
+ gateway_accepted: 1980,
+ gateway_refused: 5,
+ signoz_received: 1980,
+ signoz_accepted: 1978,
+ partial: false,
+ },
+]
+
+// ─── HopFlow SVG Component ─────────────────────────────────────────────────
+
+interface HopFlowProps {
+ hops: HopNode[]
+ links: LinkDelta[]
+ onSelectLink: (from: string, to: string) => void
+ selectedLinkId: string
+}
+
+function HopFlow({ hops, links, onSelectLink, selectedLinkId }: HopFlowProps): ReactElement {
+ return (
+
+
+
+
+
+
+
+
+
+ {/* Hop Nodes */}
+ {hops.map((hop, idx) => {
+ const x = 50 + idx * 240
+ const y = 80
+ // Size node dynamically based on accepted count percentage
+ const radius = 32 + (hop.accepted / 12000) * 12
+
+ return (
+
+ {/* Outer glow ring for status */}
+
+ {/* Node circle */}
+
+ {/* Node details */}
+
+ {hop.label}
+
+
+ {hop.accepted.toLocaleString()}
+
+
+ )
+ })}
+
+ {/* Links between hops */}
+ {links.map((link) => {
+ const fromIdx = hops.findIndex((h) => h.id === link.from)
+ const toIdx = hops.findIndex((h) => h.id === link.to)
+
+ const fromX = 50 + fromIdx * 240 + 32
+ const toX = 50 + toIdx * 240 - 32
+ const y = 80
+
+ const delta = link.sent - link.received
+ const isUnexplained = link.unexplained > 0
+ const linkId = `${link.from}->${link.to}`
+ const isSelected = selectedLinkId === linkId
+
+ // Link color: unexplained gets pulsing crit, explained gets warning, ok gets neutral/ok
+ const strokeColor = isUnexplained
+ ? 'var(--crit)'
+ : delta > 0
+ ? 'var(--warn)'
+ : 'var(--border-strong)'
+
+ return (
+ onSelectLink(link.from, link.to)}
+ className="cursor-pointer group"
+ >
+ {/* Thick invisible click helper path */}
+
+ {/* Actual path line */}
+
+ {/* Delta Badge Label */}
+
+ 0 ? 'var(--crit)' : 'var(--text-dim)',
+ fontSize: '10px',
+ }}
+ >
+ {delta > 0 ? `-${delta}` : 'ok'}
+
+
+ )
+ })}
+
+
+
+ Click link connectors to drill-down on loss decomposition metrics.
+
+
+ )
+}
+
+// ─── LinkDecompositionCard Component ───────────────────────────────────────
+
+interface DecompositionCardProps {
+ link: LinkDelta
+}
+
+function LinkDecompositionCard({ link }: DecompositionCardProps): ReactElement {
+ const delta = link.sent - link.received
+ const hasLoss = delta > 0
+
+ // Calculate percentages for stacked bars
+ const pctRefused = hasLoss ? (link.refused / delta) * 100 : 0
+ const pctEnqueue = hasLoss ? (link.enqueue_failed / delta) * 100 : 0
+ const pctSend = hasLoss ? (link.send_failed / delta) * 100 : 0
+ const pctUnexplained = hasLoss ? (link.unexplained / delta) * 100 : 0
+
+ return (
+
+
+ 0 ? 'crit' : hasLoss ? 'warn' : 'ok'}>
+ {link.unexplained > 0 ? 'UNEXPLAINED DRIFT' : hasLoss ? 'EXPLAINED LOSS' : 'NO LOSS'}
+
+ }
+ />
+
+
+ {/* Stacked bar visualization */}
+ {hasLoss ? (
+
+
+ Loss Distribution Stack ({delta} spans lost)
+
+
+ {pctRefused > 0 && (
+
+ )}
+ {pctEnqueue > 0 && (
+
+ )}
+ {pctSend > 0 && (
+
+ )}
+ {pctUnexplained > 0 && (
+
+ )}
+
+
+ ) : (
+
+ No losses or sequence errors observed on this hop.
+
+ )}
+
+ {/* Counter keyvalues */}
+
+
+
+
+ 0 ? 'Audit gap discrepancy' : 'Perfect reconciliation'}
+ />
+
+
+
+
+ )
+}
+
+// ─── Main LedgerPage ───────────────────────────────────────────────────────
+
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+
+export function LedgerPage(): ReactElement {
+ useDocumentTitle('Conservation Ledger')
+ const [selectedLinkId, setSelectedLinkId] = useState('agent->gateway')
+ const [forcePreview, setForcePreview] = useState(false)
+
+ const isSim = useMemo(() => {
+ const params = new URLSearchParams(window.location.search)
+ return params.get('data') === 'sim'
+ }, [])
+
+ const { isLoading: flowLoading, error: flowError } = useLedgerFlow(!isSim)
+
+ // Probe status state: 'loading' | 'gated' (404) | 'disabled' (409) | 'unavailable' (503) | 'live' (200)
+ const probeStatus = useMemo<
+ 'loading' | 'gated' | 'disabled' | 'unavailable' | 'live'
+ >(() => {
+ if (isSim) return 'live'
+ if (flowLoading) return 'loading'
+ if (!flowError) return 'live'
+
+ const status =
+ flowError && typeof flowError === 'object' && 'status' in flowError
+ ? (flowError as { status: number }).status
+ : null
+ if (status === 409) return 'disabled'
+ if (status === 503) return 'unavailable'
+ return 'gated'
+ }, [isSim, flowLoading, flowError])
+
+ const selectedLink = useMemo(() => {
+ return (MOCK_LINKS.find((l) => `${l.from}->${l.to}` === selectedLinkId) ??
+ MOCK_LINKS[0]) as LinkDelta
+ }, [selectedLinkId])
+
+ const sampleColumns: Column[] = [
+ {
+ key: 'interval',
+ header: 'Audit Interval',
+ render: (row) => (
+
+ {row.interval}
+ {row.partial && PARTIAL }
+
+ ),
+ },
+ {
+ key: 'agent_sent',
+ header: 'Agent Sent',
+ numeric: true,
+ render: (row) => {row.agent_sent} ,
+ },
+ {
+ key: 'gateway_received',
+ header: 'Gateway Recv',
+ numeric: true,
+ render: (row) => {row.gateway_received} ,
+ },
+ {
+ key: 'gateway_refused',
+ header: 'Gateway Refused',
+ numeric: true,
+ render: (row) => {row.gateway_refused} ,
+ },
+ {
+ key: 'signoz_received',
+ header: 'SigNoz Recv',
+ numeric: true,
+ render: (row) => {row.signoz_received} ,
+ },
+ {
+ key: 'signoz_accepted',
+ header: 'SigNoz Accepted',
+ numeric: true,
+ render: (row) => (
+
+ {row.signoz_accepted !== null ? row.signoz_accepted : '—'}
+
+ ),
+ },
+ ]
+
+ // Render Live view
+ if (probeStatus === 'live' || forcePreview) {
+ return (
+
+
+ SIMULATED
+ SAMPLING LIVE
+
+ }
+ />
+
+ {/* Horizontal Hop Flow Chart */}
+
+
+
+ setSelectedLinkId(`${from}->${to}`)}
+ selectedLinkId={selectedLinkId}
+ />
+
+
+
+ {/* Split info panels */}
+
+ {/* Left: decomposition details */}
+
+
+
+ {/* Samples table */}
+
+
+
+
row.interval}
+ loading={false}
+ />
+
+
+
+
+ {/* Right: explanations */}
+
+
+
+
+
+ What is unexplained drift?
+
+ Counters admit less loss than verification probes prove. This discrepancy occurs
+ when ground truth telemetry is dropped silently inside a pipeline hop without
+ logging.
+
+
+ Engine sampling is active:
+
+ Telemetry is reconciled per-interval, drawing on collector-side self-metrics from
+ memory nodes.
+
+
+
+
+
+
+ )
+ }
+
+ // Render 409 Ledger Disabled State
+ if (probeStatus === 'disabled') {
+ return (
+
+
+
}
+ message="Ledger is currently disabled in the backend configuration."
+ action={
+
+ # Enable ledger in spanledger.yaml:
+
+ ledger:
+
+ enabled: true
+
+ hops:
+
+ - agent
+
+ - gateway
+
+ }
+ />
+
+ )
+ }
+
+ // Render 503 Ledger Unavailable State
+ if (probeStatus === 'unavailable') {
+ return (
+
+
+
}
+ message="Collector self-metrics not reachable. Verify metric endpoints in your setup."
+ />
+
+ )
+ }
+
+ // Render 404 PendingState (gated view, static svg architecture + roadmap explanation)
+ if (probeStatus === 'gated') {
+ return (
+
+
+ coming soon
+
+ }
+ />
+
+
+
+
+
+ Hop Conservation Architecture
+
+
+ Continuous auditing reconciles sent vs accepted span counts across OTel hops to
+ expose silent drops.
+
+
+
+ {/* Static SVG of hops */}
+
+
+
+ {/* Nodes */}
+
+
+ OTel Agent
+
+
+
+
+ Telemetry Gateway
+
+
+
+
+ SigNoz UI
+
+
+ {/* Connectors */}
+
+
+ Hop 1
+
+
+
+
+ Hop 2
+
+
+
+
+
+
+
+ The Conservation Ledger reconciles sent counters against accepted counters per-hop
+ to isolate silent leakage. Spans lost silently without error logs are mapped to an{' '}
+ unexplained loss category.
+
+
+
+
+ Milestone Status: Engine sampling is live under v3 schema
+ migrations (P3-4). The granular hop decomposition API is scheduled for backend
+ milestone P3-5 . This interface will automatically un-gate once
+ the decomposition endpoint becomes queryable.
+
+
+
+
+
+ setForcePreview(true)}>
+ Preview with Mock Data
+
+
+
+
+
+ )
+ }
+
+ // Fallback Loading indicator during probe
+ return (
+
+ )
+}
diff --git a/frontend/src/pages/overview/Page.tsx b/frontend/src/pages/overview/Page.tsx
new file mode 100644
index 0000000..90e2893
--- /dev/null
+++ b/frontend/src/pages/overview/Page.tsx
@@ -0,0 +1,396 @@
+import { useSearchParams, useNavigate } from 'react-router-dom'
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardStat } from '@/components/ui/Card'
+import { StatusDot } from '@/components/ui/StatusDot'
+import { SliStat } from '@/components/slo/SliStat'
+import { BudgetGauge } from '@/components/slo/BudgetGauge'
+import { ConfidenceChip } from '@/components/slo/ConfidenceChip'
+import { Sparkline } from '@/components/ui/Sparkline'
+import { EventRow } from '@/components/events/EventRow'
+import { EmptyState } from '@/components/ui/EmptyState'
+import { SkeletonCard, SkeletonRow } from '@/components/ui/Skeleton'
+import { Select } from '@/components/ui/Select'
+import { Badge } from '@/components/ui/Badge'
+import { useStatus, useEvents, useSloHistory } from '@/api/hooks'
+import { signalHealth, burnStatus } from '@/lib/slo'
+import { formatSli, formatRatio, formatCount } from '@/lib/format'
+import { useUi } from '@/providers/UiProvider'
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+import { useEffect, useMemo, type ReactElement } from 'react'
+import { HardDrive, AlertOctagon } from 'lucide-react'
+import type { SignalType, StreamSnapshot } from '@/api/types'
+
+// ─── HeroStrip Component ───────────────────────────────────────────────────
+
+interface HeroStripProps {
+ streams: Record
+ eventsCount: number
+ loading: boolean
+}
+
+function HeroStrip({ streams, eventsCount, loading }: HeroStripProps): ReactElement {
+ const navigate = useNavigate()
+
+ const metrics = useMemo(() => {
+ const list = Object.values(streams)
+ if (list.length === 0) {
+ return { worstSli: null, worstBudget: null, totalVerified: 0 }
+ }
+
+ // 1. Find worst SLI
+ let worstSli: number | null = null
+ for (const s of list) {
+ if ('slo' in s && s.slo.sli !== null) {
+ if (worstSli === null || s.slo.sli < worstSli) {
+ worstSli = s.slo.sli
+ }
+ }
+ }
+
+ // 2. Find worst budget
+ let worstBudget: number | null = null
+ for (const s of list) {
+ if ('slo' in s && s.slo.budget_remaining_ratio !== null) {
+ if (worstBudget === null || s.slo.budget_remaining_ratio < worstBudget) {
+ worstBudget = s.slo.budget_remaining_ratio
+ }
+ }
+ }
+
+ // 3. Sum verified probes (only traces/logs snapshots carry counts)
+ let totalVerified = 0
+ for (const s of list) {
+ if ('verified' in s) {
+ totalVerified += s.verified
+ }
+ }
+
+ return { worstSli, worstBudget, totalVerified }
+ }, [streams])
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+ )
+ }
+
+ const worstSloTarget = Object.values(streams)[0]?.slo?.target ?? 0.999
+
+ return (
+
+
+
+
+
+
+
+ navigate('/timeline?class=loss')}
+ className="hover:border-border-strong cursor-pointer"
+ >
+ 0 ? 'Fault active' : 'All streams healthy'}
+ />
+
+
+
+
+
+ )
+}
+
+// ─── StreamCard Component ──────────────────────────────────────────────────
+
+interface StreamCardProps {
+ streamSnapshot: StreamSnapshot
+ signal: SignalType
+}
+
+function StreamCard({ streamSnapshot, signal }: StreamCardProps): ReactElement {
+ const navigate = useNavigate()
+ const name = streamSnapshot.stream
+
+ // History query for sparkline
+ const { data: history } = useSloHistory(name, signal, '1h')
+
+ // Derive health
+ const health = useMemo(() => {
+ return signalHealth(streamSnapshot)
+ }, [streamSnapshot])
+
+ const statusColorMap = {
+ critical: 'crit' as const,
+ warning: 'warn' as const,
+ ok: 'ok' as const,
+ unknown: 'unknown' as const,
+ }
+
+ const slo = streamSnapshot.slo
+
+ // Burn rate badges
+ const burn5m = slo.burn_rates['5m']
+ const burn1h = slo.burn_rates['1h']
+
+ const status5m = burnStatus('5m', burn5m)
+ const status1h = burnStatus('1h', burn1h)
+
+ return (
+ navigate(`/streams/${encodeURIComponent(name)}`)}
+ className="cursor-pointer hover:border-border-strong hover:bg-surface-2 transition-colors flex flex-col justify-between"
+ >
+
+
+
+
+
+ {name}
+
+
+
+
+ {burn5m >= 2.0 && (
+
+ 5m: {burn5m.toFixed(1)}x
+
+ )}
+ {burn1h >= 2.0 && (
+
+ 1h: {burn1h.toFixed(1)}x
+
+ )}
+
+
+
+ {'delivery' in streamSnapshot ? (
+ /* Metrics max-stagnation model display */
+
+
+
+ {streamSnapshot.delivery === 'advancing' ? 'Advancing' : 'Stalled'}
+
+
+ coarser SLI: max-stagnation
+
+
+
+ Observed Max: {streamSnapshot.observed_max} ·
+ Previous Max: {streamSnapshot.prev_max}
+
+
+ ) : (
+ /* Standard probe-based delivery ratio display */
+
+
+
+ )}
+
+
+
+
+
+
+
+ Budget Remaining
+
+
+ {formatRatio(slo.budget_remaining_ratio)}
+
+
+
+
+ {/* 1h trend Sparkline */}
+
+
+
+
+
+ )
+}
+
+// ─── Main OverviewPage ─────────────────────────────────────────────────────
+
+export function OverviewPage(): ReactElement {
+ useDocumentTitle('Overview')
+ const [searchParams, setSearchParams] = useSearchParams()
+ const navigate = useNavigate()
+ const { setSignal: setGlobalSignal } = useUi()
+
+ // Get active signal from URL param, defaulting to traces
+ const signal = (searchParams.get('signal') || 'traces') as SignalType
+
+ // Sync to UiProvider
+ useEffect(() => {
+ setGlobalSignal(signal)
+ }, [signal, setGlobalSignal])
+
+ // Queries
+ const { data: status, isLoading: statusLoading } = useStatus(signal)
+ const { data: eventsData, isLoading: eventsLoading } = useEvents({ limit: 8 })
+
+ const handleSignalChange = (val: string) => {
+ setSearchParams({ signal: val })
+ }
+
+ // Derive active incidents count from current events list
+ const activeIncidentsCount = useMemo(() => {
+ if (!eventsData) return 0
+ // An active incident is an open loss event (not recovered)
+ // Filter events to find all loss findings
+ const events = eventsData.pages.flatMap((p) => p.events)
+ const lossEvents = events.filter((e) => e.class === 'loss')
+ const recoveryEvents = events.filter((e) => e.class === 'recovery')
+
+ let count = 0
+ for (const le of lossEvents) {
+ // Find if there is any recovery event linking to this le.id
+ const recovered = recoveryEvents.some(
+ (re) => re.kind === 'enveloped' && Array.isArray(re.links) && re.links.includes(le.id)
+ )
+ if (!recovered) {
+ count++
+ }
+ }
+ return count
+ }, [eventsData])
+
+ const streams = status?.streams ?? {}
+ const streamList = Object.values(streams)
+ const events = eventsData?.pages.flatMap((p) => p.events) ?? []
+
+ // Main loading state
+ const isFirstLoad = statusLoading && !status
+
+ return (
+
+ }
+ />
+
+ {/* Hero indicators */}
+
+
+
+ {/* Left: 2-col streams grid */}
+
+
+ Telemetry Streams
+
+
+ {isFirstLoad ? (
+
+
+
+
+ ) : streamList.length === 0 ? (
+
}
+ message="No streams reporting yet — spanLedger is starting its first verification cycle (~1 min)."
+ />
+ ) : (
+
+ {streamList.map((stream) => (
+
+ ))}
+
+ )}
+
+
+ {/* Right: Recent Events Rail */}
+
+
+
+ Recent Events
+
+ navigate('/timeline')}
+ className="text-xs font-semibold hover:underline"
+ style={{ color: 'var(--accent)' }}
+ >
+ View timeline →
+
+
+
+ {eventsLoading && !eventsData ? (
+
+
+
+
+
+ ) : events.length === 0 ? (
+
}
+ message="No reliability events — the pipeline is delivering."
+ />
+ ) : (
+
+ {events.slice(0, 8).map((ev) => (
+ {
+ if (ev.kind === 'finding') {
+ navigate(`/incidents/${encodeURIComponent(ev.id)}`)
+ } else {
+ navigate(`/timeline?highlight=${encodeURIComponent(ev.id)}`)
+ }
+ }}
+ />
+ ))}
+
+ )}
+
+
+
+ )
+}
diff --git a/frontend/src/pages/reliability/Page.tsx b/frontend/src/pages/reliability/Page.tsx
new file mode 100644
index 0000000..9feab5c
--- /dev/null
+++ b/frontend/src/pages/reliability/Page.tsx
@@ -0,0 +1,334 @@
+import { useSearchParams, useNavigate } from 'react-router-dom'
+import { useMemo, useState, type ReactElement } from 'react'
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardHeader, CardStat } from '@/components/ui/Card'
+import { Table, type Column } from '@/components/ui/Table'
+import { StatusDot } from '@/components/ui/StatusDot'
+import { SliStat } from '@/components/slo/SliStat'
+import { BurnRateBars } from '@/components/slo/BurnRateBars'
+import { Badge } from '@/components/ui/Badge'
+import { Select } from '@/components/ui/Select'
+import { EmptyState } from '@/components/ui/EmptyState'
+import { EventRow } from '@/components/events/EventRow'
+import { useStatus, useEvents } from '@/api/hooks'
+import { signalHealth } from '@/lib/slo'
+import { formatSli, formatRatio } from '@/lib/format'
+import { ShieldCheck } from 'lucide-react'
+import type { SignalType } from '@/api/types'
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+
+type SortField = 'stream' | 'budget'
+type SortDir = 'asc' | 'desc'
+
+export function ReliabilityPage(): ReactElement {
+ useDocumentTitle('Reliability Summary')
+ const [searchParams, setSearchParams] = useSearchParams()
+ const navigate = useNavigate()
+
+ // Get active signal from URL, defaulting to traces
+ const signal = (searchParams.get('signal') || 'traces') as SignalType
+
+ // Queries
+ const { data: status, isLoading: statusLoading } = useStatus(signal)
+ const { data: eventsData, isLoading: eventsLoading } = useEvents({ limit: 100 })
+
+ // Sort states
+ const [sortKey, setSortKey] = useState('stream')
+ const [sortDir, setSortDir] = useState('asc')
+
+ const handleSignalChange = (val: string) => {
+ setSearchParams({ signal: val })
+ }
+
+ // Derive stream rows with health details
+ const streamRows = useMemo(() => {
+ if (!status?.streams) return []
+
+ const list = Object.entries(status.streams).map(([name, snapshot]) => {
+ const health = signalHealth(snapshot)
+ return {
+ name,
+ health,
+ sli: snapshot.slo.sli,
+ target: snapshot.slo.target,
+ budgetRatio: snapshot.slo.budget_remaining_ratio,
+ burnRates: snapshot.slo.burn_rates,
+ snapshot,
+ }
+ })
+
+ // Apply sorting
+ return list.sort((a, b) => {
+ let comparison = 0
+ if (sortKey === 'stream') {
+ comparison = a.name.localeCompare(b.name)
+ } else if (sortKey === 'budget') {
+ comparison = a.budgetRatio - b.budgetRatio
+ }
+
+ return sortDir === 'asc' ? comparison : -comparison
+ })
+ }, [status, sortKey, sortDir])
+
+ const activeIncidents = useMemo(() => {
+ if (!eventsData) return []
+ const events = eventsData.pages.flatMap((p) => p.events)
+ const lossEvents = events.filter((e) => e.class === 'loss')
+ const recoveryEvents = events.filter((e) => e.class === 'recovery')
+ return lossEvents.filter((le) => {
+ return !recoveryEvents.some(
+ (re) => re.kind === 'enveloped' && Array.isArray(re.links) && re.links.includes(le.id)
+ )
+ })
+ }, [eventsData])
+
+ const handleSort = (key: string) => {
+ if (key === 'stream' || key === 'budget') {
+ if (sortKey === key) {
+ setSortDir(sortDir === 'asc' ? 'desc' : 'asc')
+ } else {
+ setSortKey(key)
+ setSortDir('asc')
+ }
+ }
+ }
+
+ // Status badge mappings
+ const healthBadgeVariant = (health: 'critical' | 'warning' | 'ok' | 'unknown') => {
+ switch (health) {
+ case 'critical':
+ return 'crit' as const
+ case 'warning':
+ return 'warn' as const
+ case 'ok':
+ return 'ok' as const
+ default:
+ return 'neutral' as const
+ }
+ }
+
+ const columns: Column<(typeof streamRows)[number]>[] = [
+ {
+ key: 'stream',
+ header: 'Stream',
+ sortable: true,
+ render: (row) => (
+
+
+ {row.name}
+
+ ),
+ },
+ {
+ key: 'sli',
+ header: 'SLI (Observed)',
+ numeric: true,
+ render: (row) => {
+ if ('delivery' in row.snapshot) {
+ return (
+
+ {row.snapshot.delivery === 'advancing' ? 'Advancing' : 'Stalled'}
+
+ )
+ }
+ return
+ },
+ },
+ {
+ key: 'target',
+ header: 'SLO Target',
+ numeric: true,
+ render: (row) => {
+ const isMetrics = 'delivery' in row.snapshot
+ if (isMetrics) return —
+ return {formatSli(row.target)}
+ },
+ },
+ {
+ key: 'status',
+ header: 'Status',
+ render: (row) => (
+ {row.health.toUpperCase()}
+ ),
+ },
+ {
+ key: 'budget',
+ header: 'Remaining Budget',
+ sortable: true,
+ numeric: true,
+ render: (row) => (
+
+ {formatRatio(row.budgetRatio)}
+
+ ),
+ },
+ {
+ key: 'burn',
+ header: 'Burn Rates',
+ width: '260px',
+ render: (row) => ,
+ },
+ ]
+
+ const worstSloTarget = Object.values(status?.streams ?? {})[0]?.slo?.target ?? 0.999
+
+ // Calculate summary metrics
+ const summaryStats = useMemo(() => {
+ if (streamRows.length === 0) return { avgSli: null, worstBudget: null, totalBurnExceeded: 0 }
+
+ let sliSum = 0
+ let sliCount = 0
+ let worstBudget = 1.0
+ let totalBurnExceeded = 0
+
+ for (const r of streamRows) {
+ if (r.sli !== null && r.sli !== undefined) {
+ sliSum += r.sli
+ sliCount++
+ }
+ if (r.budgetRatio < worstBudget) {
+ worstBudget = r.budgetRatio
+ }
+ // Count streams with > 14.4x burn rate in 5m or 1h
+ const burn5m = r.burnRates['5m'] ?? 0
+ const burn1h = r.burnRates['1h'] ?? 0
+ if (burn5m >= 14.4 || burn1h >= 14.4) {
+ totalBurnExceeded++
+ }
+ }
+
+ return {
+ avgSli: sliCount > 0 ? sliSum / sliCount : null,
+ worstBudget,
+ totalBurnExceeded,
+ }
+ }, [streamRows])
+
+ return (
+
+ }
+ />
+
+ {/* Summary KPI Strip */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* SLO Status Grid Table */}
+
+
+
+
+
r.name}
+ rowHref={(r) => `/streams/${encodeURIComponent(r.name)}`}
+ sortKey={sortKey}
+ sortDir={sortDir}
+ onSort={handleSort}
+ loading={statusLoading && streamRows.length === 0}
+ empty={
+ }
+ message="No telemetry streams observed yet."
+ />
+ }
+ />
+
+
+
+
+ {/* Active Incidents section at the bottom */}
+
+
+ Active SLO Incidents
+
+
+ {eventsLoading && activeIncidents.length === 0 ? (
+
+ ) : activeIncidents.length === 0 ? (
+
}
+ message="No active incidents — all pipelines are running within budget margins."
+ />
+ ) : (
+
+ {activeIncidents.map((ev) => (
+ navigate(`/incidents/${encodeURIComponent(ev.id)}`)}
+ />
+ ))}
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/pages/settings/Page.tsx b/frontend/src/pages/settings/Page.tsx
new file mode 100644
index 0000000..f9a37ab
--- /dev/null
+++ b/frontend/src/pages/settings/Page.tsx
@@ -0,0 +1,261 @@
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardHeader } from '@/components/ui/Card'
+import { Button } from '@/components/ui/Button'
+import { Badge } from '@/components/ui/Badge'
+import { useUi } from '@/providers/UiProvider'
+import { getSettings, saveSettings, type Settings } from '@/lib/settings'
+import { apiGet } from '@/api/client'
+import { useState, useEffect, type ReactElement } from 'react'
+import { Save, RefreshCw, Activity, CheckCircle2, AlertOctagon } from 'lucide-react'
+
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+
+export function SettingsPage(): ReactElement {
+ useDocumentTitle('Settings')
+ const { reducedMotion, presentationMode, togglePresentationMode } = useUi()
+ const currentSettings = getSettings()
+
+ const [apiBaseUrl, setApiBaseUrl] = useState(currentSettings.apiBaseUrl)
+ const [signozBaseUrl, setSignozBaseUrl] = useState(currentSettings.signozBaseUrl)
+ const [pollMultiplier, setPollMultiplier] = useState(currentSettings.pollIntervalMultiplier)
+ const [isSaved, setIsSaved] = useState(false)
+ const [testing, setTesting] = useState(false)
+ const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
+
+ useEffect(() => {
+ if (!isSaved) return
+ const timer = setTimeout(() => setIsSaved(false), 4000)
+ return () => clearTimeout(timer)
+ }, [isSaved])
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const updates: Partial = {
+ apiBaseUrl: apiBaseUrl.trim(),
+ signozBaseUrl: signozBaseUrl.trim(),
+ pollIntervalMultiplier: Number(pollMultiplier),
+ }
+
+ saveSettings(updates)
+ setIsSaved(true)
+ }
+
+ const handleTestConnection = async () => {
+ setTesting(true)
+ setTestResult(null)
+ try {
+ const res = await apiGet<{ status: string }>('/healthz')
+ if (res && res.status === 'ok') {
+ setTestResult({ success: true, message: 'Connected to spanLedger /healthz (status: ok)' })
+ } else {
+ setTestResult({ success: false, message: `Unexpected response: ${JSON.stringify(res)}` })
+ }
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : String(err)
+ setTestResult({ success: false, message: `Connection failed: ${msg}` })
+ } finally {
+ setTesting(false)
+ }
+ }
+
+ const handleReset = () => {
+ setApiBaseUrl('')
+ setSignozBaseUrl('http://localhost:8080')
+ setPollMultiplier(1)
+ setTestResult(null)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ spanLedger API Base URL
+
+
setApiBaseUrl(e.target.value)}
+ className="px-3 py-2 text-sm rounded-sm border focus:outline-none w-full"
+ style={{
+ backgroundColor: 'var(--surface-2)',
+ borderColor: 'var(--border-strong)',
+ color: 'var(--text)',
+ height: '36px',
+ }}
+ />
+
+ Empty by default. If specified, queries will hit this target host directly.
+
+
+
+ Test Connection
+
+ {testResult && (
+
+ {testResult.success ?
:
}
+
{testResult.message}
+
+ )}
+
+
+
+
+
+ Poll Interval Multiplier
+
+ setPollMultiplier(Number(e.target.value))}
+ className="px-3 py-2 text-sm rounded-sm border focus:outline-none w-full"
+ style={{
+ backgroundColor: 'var(--surface-2)',
+ borderColor: 'var(--border-strong)',
+ color: 'var(--text)',
+ height: '36px',
+ }}
+ />
+
+ Multiplier for polling frequencies. E.g. 2.0 doubles interval timings (slowing
+ queries).
+
+
+
+
+
+
+
+
+
+
+ SigNoz UI Base URL
+
+ setSignozBaseUrl(e.target.value)}
+ required
+ className="px-3 py-2 text-sm rounded-sm border focus:outline-none w-full"
+ style={{
+ backgroundColor: 'var(--surface-2)',
+ borderColor: 'var(--border-strong)',
+ color: 'var(--text)',
+ height: '36px',
+ }}
+ />
+
+ Base address for trace correlation explorer deep-links.
+
+
+
+
+
+
+
+
+
+
+
+ Presentation Mode (Projectors)
+
+
+ Scales type ramp up 15%, collapses sidebar to icon rail, forces background refetch. Toggle via Shift+P.
+
+
+
+ {presentationMode ? 'ACTIVE (Shift+P)' : 'ENABLE (Shift+P)'}
+
+
+
+
+
+ Reduced Motion
+
+
+ Derives motion reduction preferences from your operating system settings.
+
+
+
+ {reducedMotion ? 'ACTIVE' : 'INACTIVE'}
+
+
+
+
+
+ {isSaved && (
+
+
+ Settings saved successfully! Some changes may require reloading the browser tab.
+
+ window.location.reload()}
+ className="font-semibold underline ml-auto flex items-center gap-1 hover:text-text focus:outline-none"
+ >
+ Reload Page
+
+
+ )}
+
+
+
+ Save Settings
+
+
+ Reset to Defaults
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/stream/Page.tsx b/frontend/src/pages/stream/Page.tsx
new file mode 100644
index 0000000..e9a76c7
--- /dev/null
+++ b/frontend/src/pages/stream/Page.tsx
@@ -0,0 +1,304 @@
+import { useParams, useSearchParams, useNavigate } from 'react-router-dom'
+import { PageHeader } from '@/components/layout/PageHeader'
+import { Card, CardHeader, CardStat } from '@/components/ui/Card'
+import { SliHistoryChart } from '@/components/charts/SliHistoryChart'
+import { BurnRateBars } from '@/components/slo/BurnRateBars'
+import { EtaChip } from '@/components/slo/EtaChip'
+import { StatusDot } from '@/components/ui/StatusDot'
+import { Badge } from '@/components/ui/Badge'
+import { SliStat } from '@/components/slo/SliStat'
+import { Select } from '@/components/ui/Select'
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs'
+import { EmptyState } from '@/components/ui/EmptyState'
+import { EventRow } from '@/components/events/EventRow'
+import { KeyValue } from '@/components/ui/KeyValue'
+import { SkeletonCard, SkeletonRow } from '@/components/ui/Skeleton'
+import { useStream, useSloHistory, useEvents } from '@/api/hooks'
+import { signalHealth } from '@/lib/slo'
+import { formatSli, formatRatio } from '@/lib/format'
+import { useState, useMemo, type ReactElement } from 'react'
+import { ShieldCheck } from 'lucide-react'
+import type {
+ SignalType,
+ StreamSnapshot,
+ TracesStreamSnapshot,
+ MetricsStreamSnapshot,
+} from '@/api/types'
+
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+
+export function StreamPage(): ReactElement {
+ const { name } = useParams<{ name: string }>()
+ useDocumentTitle(name ? `${name} · Stream Details` : 'Stream Details')
+ const [searchParams, setSearchParams] = useSearchParams()
+ const navigate = useNavigate()
+
+ // Get active signal from URL, defaulting to traces
+ const signal = (searchParams.get('signal') || 'traces') as SignalType
+
+ // Resolution toggle
+ const [resolution, setResolution] = useState<'1h' | '1d'>('1h')
+
+ // Queries
+ const {
+ data: stream,
+ isLoading: streamLoading,
+ error: streamError,
+ } = useStream(name || '', signal)
+ const { data: history, isLoading: historyLoading } = useSloHistory(name || '', signal, resolution)
+ const { data: eventsData, isLoading: eventsLoading } = useEvents({
+ stream: name || '',
+ limit: 50,
+ })
+
+ const handleSignalChange = (val: string) => {
+ setSearchParams({ signal: val })
+ }
+
+ // Derive stream health
+ const health = useMemo(() => {
+ if (!stream) return 'unknown' as const
+ return signalHealth(stream as StreamSnapshot)
+ }, [stream])
+
+ const statusColorMap = {
+ critical: 'crit' as const,
+ warning: 'warn' as const,
+ ok: 'ok' as const,
+ unknown: 'unknown' as const,
+ }
+
+ const events = useMemo(() => {
+ return eventsData?.pages.flatMap((p) => p.events) ?? []
+ }, [eventsData])
+
+ // Count incidents (active and resolved)
+ const incidentsCount = useMemo(() => {
+ return events.filter((e) => e.class === 'loss').length
+ }, [events])
+
+ if (streamLoading) {
+ return (
+
+ )
+ }
+
+ if (streamError || !stream || !name) {
+ return (
+
+ Error loading stream details or stream "{name}" not found.
+
+ )
+ }
+
+ const isMetrics = 'delivery' in stream
+ const metricsStream = isMetrics ? (stream as MetricsStreamSnapshot) : null
+ const tracesStream = !isMetrics ? (stream as TracesStreamSnapshot) : null
+ const slo = stream.slo
+
+ return (
+
+
+
+
+
+
+
+ {health.toUpperCase()}
+
+
+ }
+ />
+
+ {/* KPI Strip */}
+
+
+
+ {metricsStream.delivery}
+
+ ) : (
+
+ )
+ }
+ context={isMetrics ? 'Delivery Advancing' : `Target: ${formatSli(slo.target)}`}
+ />
+
+
+
+
+
+
+
+
+
+
+ Exhaustion ETA
+
+
+ {slo.exhaustion_eta_hours !== null ? (
+
+ ) : (
+
+ No burn detected
+
+ )}
+
+
+
+
+
+
+ {/* Left main charts & events (2 cols) */}
+
+ {/* History Chart Card */}
+
+
+
+ SLI Verification History
+
+ setResolution(r as '1h' | '1d')}>
+
+ 24 Hours (1h Resolution)
+ 30 Days (1d Resolution)
+
+
+
+
+
+ {historyLoading ? (
+
+ ) : !history || history.length === 0 ? (
+
+ No historical buckets collected.
+
+ ) : (
+
+ )}
+
+
+
+ {/* Events Log list */}
+
+
+ Stream Incident Log
+
+
+ {eventsLoading ? (
+
+
+
+
+ ) : events.length === 0 ? (
+
}
+ message="No reliability events recorded for this stream."
+ />
+ ) : (
+
+ {events.map((ev) => (
+ {
+ if (ev.class === 'loss') {
+ navigate(`/incidents/${encodeURIComponent(ev.id)}`)
+ } else {
+ navigate(`/timeline?highlight=${encodeURIComponent(ev.id)}`)
+ }
+ }}
+ />
+ ))}
+
+ )}
+
+
+
+ {/* Right configuration sidebar (1 col) */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isMetrics && metricsStream ? (
+ <>
+
+
+ >
+ ) : tracesStream ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/timeline/Page.tsx b/frontend/src/pages/timeline/Page.tsx
new file mode 100644
index 0000000..6041c96
--- /dev/null
+++ b/frontend/src/pages/timeline/Page.tsx
@@ -0,0 +1,280 @@
+import { useSearchParams, useNavigate } from 'react-router-dom'
+import { PageHeader } from '@/components/layout/PageHeader'
+import { EventRow } from '@/components/events/EventRow'
+import { FilterChips } from '@/components/ui/FilterChips'
+import { Select } from '@/components/ui/Select'
+import { Button } from '@/components/ui/Button'
+import { EmptyState } from '@/components/ui/EmptyState'
+import { SkeletonRow } from '@/components/ui/Skeleton'
+import { useEvents, useStatus } from '@/api/hooks'
+import { AlertOctagon } from 'lucide-react'
+import { useMemo, type ReactElement } from 'react'
+
+const CLASS_OPTIONS = [
+ { value: 'loss', label: 'Loss' },
+ { value: 'recovery', label: 'Recovery' },
+ { value: 'budget_warning', label: 'Budget Warning' },
+ { value: 'budget_exhausted', label: 'Budget Exhausted' },
+ { value: 'burn_rate_high', label: 'Burn Rate High' },
+ { value: 'deploy_marker', label: 'Deploy Marker' },
+ { value: 'epoch_orphaned', label: 'Restart' },
+ { value: 'backend_unreachable', label: 'Backend Unreachable' },
+]
+
+const SEVERITY_OPTIONS = [
+ { value: 'all', label: 'All Severities' },
+ { value: 'critical', label: 'Critical' },
+ { value: 'warning', label: 'Warning' },
+ { value: 'info', label: 'Info' },
+]
+
+import { useDocumentTitle } from '@/hooks/useDocumentTitle'
+
+export function TimelinePage(): ReactElement {
+ useDocumentTitle('Event Timeline')
+ const [searchParams, setSearchParams] = useSearchParams()
+ const navigate = useNavigate()
+
+ // Get filter parameters from URL
+ const selectedClass = searchParams.get('class') || null
+ const selectedSeverity = searchParams.get('severity') || 'all'
+ const selectedStream = searchParams.get('stream') || 'all'
+ const highlightedId = searchParams.get('highlight') || null
+
+ // Fetch active streams to populate stream selector
+ const { data: status } = useStatus()
+ const streamOptions = useMemo(() => {
+ const base = [{ value: 'all', label: 'All Streams' }]
+ if (status?.streams) {
+ for (const name of Object.keys(status.streams)) {
+ base.push({ value: name, label: name })
+ }
+ }
+ return base
+ }, [status])
+
+ // Construct filters object for api query
+ const apiFilters = useMemo(() => {
+ const f: Record = { limit: 50 }
+ if (selectedStream !== 'all') f.stream = selectedStream
+ if (selectedClass) f.class = selectedClass
+ if (selectedSeverity !== 'all') f.severity = selectedSeverity
+ return f
+ }, [selectedStream, selectedClass, selectedSeverity])
+
+ // Infinite query for events
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } =
+ useEvents(apiFilters)
+
+ const events = useMemo(() => {
+ return data?.pages.flatMap((page) => page.events) ?? []
+ }, [data])
+
+ // Group events by local date
+ const groupedEvents = useMemo(() => {
+ const groups: { date: string; events: typeof events }[] = []
+ for (const ev of events) {
+ const dateStr = new Date(ev.emittedAtMs).toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+ let group = groups.find((g) => g.date === dateStr)
+ if (!group) {
+ group = { date: dateStr, events: [] }
+ groups.push(group)
+ }
+ group.events.push(ev)
+ }
+ return groups
+ }, [events])
+
+ const handleClassChange = (value: string | null) => {
+ const nextParams = new URLSearchParams(searchParams)
+ if (value) {
+ nextParams.set('class', value)
+ } else {
+ nextParams.delete('class')
+ }
+ setSearchParams(nextParams)
+ }
+
+ const handleSeverityChange = (value: string) => {
+ const nextParams = new URLSearchParams(searchParams)
+ if (value && value !== 'all') {
+ nextParams.set('severity', value)
+ } else {
+ nextParams.delete('severity')
+ }
+ setSearchParams(nextParams)
+ }
+
+ const handleStreamChange = (value: string) => {
+ const nextParams = new URLSearchParams(searchParams)
+ if (value && value !== 'all') {
+ nextParams.set('stream', value)
+ } else {
+ nextParams.delete('stream')
+ }
+ setSearchParams(nextParams)
+ }
+
+ const clearFilters = () => {
+ setSearchParams({})
+ }
+
+ const isFiltered =
+ selectedClass !== null || selectedSeverity !== 'all' || selectedStream !== 'all'
+
+ return (
+
+
+
+ {/* Filter panel */}
+
+
+
+ {isFiltered && (
+
+ Clear Filters
+
+ )}
+
+
+
+
+ Filter by Event Class
+
+
+
+
+
+ {/* Timeline output */}
+
+ {isLoading ? (
+
+
+
+
+
+ ) : isError ? (
+
+ Error loading timeline. Please check your backend connection.
+
+ ) : events.length === 0 ? (
+
+ {isFiltered ? (
+ }
+ message="No events match these filters."
+ action={
+
+ Reset filters
+
+ }
+ />
+ ) : (
+ }
+ message="No reliability events — the pipeline is delivering."
+ />
+ )}
+
+ ) : (
+
+ {groupedEvents.map((group) => (
+
+ {/* Date marker block */}
+
+
+ {group.date}
+
+
+
+ {/* Event list */}
+
+ {group.events.map((ev) => (
+ {
+ if (ev.class === 'loss') {
+ navigate(`/incidents/${encodeURIComponent(ev.id)}`)
+ } else if (ev.class === 'recovery') {
+ const targetLossId = ev.links[0]
+ if (targetLossId) {
+ navigate(`/incidents/${encodeURIComponent(targetLossId)}`)
+ } else {
+ navigate(`/timeline?highlight=${encodeURIComponent(ev.id)}`)
+ }
+ } else if (ev.class === 'deploy_marker') {
+ navigate(`/deploys?highlight=${encodeURIComponent(ev.id)}`)
+ } else {
+ // Standard detail highlight toggle
+ const nextParams = new URLSearchParams(searchParams)
+ nextParams.set('highlight', ev.id)
+ setSearchParams(nextParams)
+ }
+ }}
+ />
+ ))}
+
+
+ ))}
+
+ {/* Load more action button */}
+ {hasNextPage && (
+
+ void fetchNextPage()}
+ >
+ Load More Events
+
+
+ )}
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/providers/DemoProvider.tsx b/frontend/src/providers/DemoProvider.tsx
new file mode 100644
index 0000000..6e339dc
--- /dev/null
+++ b/frontend/src/providers/DemoProvider.tsx
@@ -0,0 +1,39 @@
+/**
+ * DemoProvider — demo/presenter mode state.
+ * Mounted only under /demo — nowhere else uses this context.
+ */
+import { createContext, useContext, useState, type ReactNode } from 'react'
+
+interface DemoContextValue {
+ /** Current walkthrough step (1-indexed) */
+ step: number
+ setStep: (step: number) => void
+ /** True when presenter fullscreen mode is active */
+ presenterMode: boolean
+ setPresenterMode: (active: boolean) => void
+ /** True when using simulated-data source instead of live backend */
+ simulated: boolean
+ setSimulated: (sim: boolean) => void
+}
+
+const DemoContext = createContext(null)
+
+export function DemoProvider({ children }: { children: ReactNode }) {
+ const [step, setStep] = useState(1)
+ const [presenterMode, setPresenterMode] = useState(false)
+ const [simulated, setSimulated] = useState(false)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useDemo(): DemoContextValue {
+ const ctx = useContext(DemoContext)
+ if (!ctx) throw new Error('useDemo must be used within DemoProvider')
+ return ctx
+}
diff --git a/frontend/src/providers/UiProvider.tsx b/frontend/src/providers/UiProvider.tsx
new file mode 100644
index 0000000..a565029
--- /dev/null
+++ b/frontend/src/providers/UiProvider.tsx
@@ -0,0 +1,96 @@
+/**
+ * UiProvider — global UI state (~40 lines as spec'd).
+ *
+ * Manages:
+ * - Active signal (mirrors URL param ?signal=)
+ * - Reduced-motion flag (from prefers-reduced-motion)
+ * - Connection status (derived from query error states)
+ */
+import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
+import type { SignalType } from '@/api/types'
+
+const PRESENTATION_KEY = 'spanledger.ui.presentation'
+
+interface UiContextValue {
+ /** Currently selected signal; mirrors ?signal= URL param */
+ signal: SignalType
+ setSignal: (s: SignalType) => void
+ /** True when prefers-reduced-motion is set */
+ reducedMotion: boolean
+ /** True when ≥ 2 consecutive healthz failures have been observed */
+ connectionDown: boolean
+ setConnectionDown: (down: boolean) => void
+ /** Presentation mode for projectors */
+ presentationMode: boolean
+ togglePresentationMode: () => void
+ /** Mobile sheet navigation menu state (<768px) */
+ mobileMenuOpen: boolean
+ setMobileMenuOpen: (open: boolean) => void
+}
+
+const UiContext = createContext(null)
+
+export function UiProvider({ children }: { children: ReactNode }) {
+ const [signal, setSignal] = useState('traces')
+ const [reducedMotion, setReducedMotion] = useState(false)
+ const [connectionDown, setConnectionDown] = useState(false)
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
+ const [presentationMode, setPresentationMode] = useState(() => {
+ try {
+ return localStorage.getItem(PRESENTATION_KEY) === 'true'
+ } catch {
+ return false
+ }
+ })
+
+ // Sync presentation attribute to html
+ useEffect(() => {
+ document.documentElement.setAttribute('data-presentation', presentationMode ? 'true' : 'false')
+ try {
+ localStorage.setItem(PRESENTATION_KEY, String(presentationMode))
+ } catch {
+ // localStorage disabled
+ }
+ }, [presentationMode])
+
+ // Sync reduced-motion from OS preference
+ useEffect(() => {
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
+ setReducedMotion(mq.matches)
+ const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches)
+ mq.addEventListener('change', handler)
+ return () => mq.removeEventListener('change', handler)
+ }, [])
+
+ const handleSetSignal = useCallback((s: SignalType) => {
+ setSignal(s)
+ }, [])
+
+ const togglePresentationMode = useCallback(() => {
+ setPresentationMode((prev) => !prev)
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useUi(): UiContextValue {
+ const ctx = useContext(UiContext)
+ if (!ctx) throw new Error('useUi must be used within UiProvider')
+ return ctx
+}
diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx
new file mode 100644
index 0000000..41926e5
--- /dev/null
+++ b/frontend/src/router.tsx
@@ -0,0 +1,100 @@
+/**
+ * Router — all 9 routes per FRONTEND_ARCHITECTURE.md.
+ *
+ * Routes:
+ * / Overview
+ * /reliability Reliability Summary
+ * /timeline Event Timeline
+ * /incidents/:id Incident Details
+ * /deploys Deploy Timeline
+ * /streams/:name Stream Details
+ * /ledger Conservation Ledger (gated — backend pending)
+ * /settings Settings
+ * /demo Demo Mode (presenter walkthrough)
+ * /kitchen-sink Dev-only component showcase
+ * * NotFound
+ *
+ * Code splitting: /demo and /ledger are lazy-loaded.
+ * All other routes are in the main bundle.
+ */
+import { createBrowserRouter, type RouteObject } from 'react-router-dom'
+import { lazy, Suspense } from 'react'
+import { RootLayout } from '@/components/layout/RootLayout'
+import { ErrorPanel } from '@/components/ui/ErrorPanel'
+import { NotFound } from '@/pages/NotFound'
+
+// Lazy-loaded routes (code splitting per architecture docs)
+const DemoPage = lazy(() => import('@/pages/demo/Page').then((m) => ({ default: m.DemoPage })))
+const LedgerPage = lazy(() =>
+ import('@/pages/ledger/Page').then((m) => ({ default: m.LedgerPage }))
+)
+
+// All other pages — in main bundle (small; no extra spinners)
+import { OverviewPage } from '@/pages/overview/Page'
+import { ReliabilityPage } from '@/pages/reliability/Page'
+import { TimelinePage } from '@/pages/timeline/Page'
+import { IncidentPage } from '@/pages/incident/Page'
+import { DeploysPage } from '@/pages/deploys/Page'
+import { StreamPage } from '@/pages/stream/Page'
+import { SettingsPage } from '@/pages/settings/Page'
+import { KitchenSinkPage } from '@/pages/kitchen-sink/Page'
+
+function LazyPage({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ }
+ >
+ {children}
+
+ )
+}
+
+const mainChildren: RouteObject[] = [
+ { index: true, element: },
+ { path: 'reliability', element: },
+ { path: 'timeline', element: },
+ { path: 'incidents/:id', element: },
+ { path: 'deploys', element: },
+ { path: 'streams', element: },
+ { path: 'streams/:name', element: },
+ {
+ path: 'ledger',
+ element: (
+
+
+
+ ),
+ },
+ { path: 'settings', element: },
+]
+
+if (import.meta.env.DEV) {
+ mainChildren.push({ path: 'kitchen-sink', element: })
+}
+
+mainChildren.push({ path: '*', element: })
+
+const routes: RouteObject[] = [
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: mainChildren,
+ },
+ {
+ // Demo renders full-bleed (presenter mode owns the whole viewport)
+ path: '/demo',
+ element: (
+
+
+
+ ),
+ errorElement: ,
+ },
+]
+
+export const router = createBrowserRouter(routes)
diff --git a/frontend/src/test/fixtures/backend-unreachable.json b/frontend/src/test/fixtures/backend-unreachable.json
new file mode 100644
index 0000000..c1fb4b4
--- /dev/null
+++ b/frontend/src/test/fixtures/backend-unreachable.json
@@ -0,0 +1,8 @@
+{
+ "id": "01hw3x4j5k6m7n8p9q0t",
+ "stream": "gateway-a",
+ "epoch": "01hw3x0000000000000000",
+ "class": "backend_unreachable",
+ "signal": "traces",
+ "detail": "Connection refused: otel-gateway:4317"
+}
diff --git a/frontend/src/test/fixtures/loss-finding.json b/frontend/src/test/fixtures/loss-finding.json
new file mode 100644
index 0000000..7dc5fe1
--- /dev/null
+++ b/frontend/src/test/fixtures/loss-finding.json
@@ -0,0 +1,33 @@
+{
+ "id": "01hw3x4j5k6m7n8p9q0r",
+ "stream": "gateway-a",
+ "epoch": "01hw3x0000000000000000",
+ "class": "loss",
+ "signal": "traces",
+ "window": {
+ "from": "2024-01-15T10:00:00Z",
+ "to": "2024-01-15T10:10:00Z"
+ },
+ "probes": {
+ "sent": 600,
+ "verified": 540,
+ "missing": 55,
+ "duplicate": 5,
+ "unknown": 0
+ },
+ "delivery_ratio": 0.908,
+ "loss_onset": "2024-01-15T10:02:30Z",
+ "gap_runs": [
+ {
+ "seq_from": 142,
+ "seq_to": 196,
+ "t_from": "2024-01-15T10:02:21Z",
+ "t_to": "2024-01-15T10:03:16Z"
+ }
+ ],
+ "gap_shape": "contiguous",
+ "extrapolated_user_spans_lost": 110,
+ "correlation_hint": null,
+ "confidence": "high",
+ "traces_filter": "spanledger.stream = 'gateway-a' AND spanledger.seq >= 142 AND spanledger.seq <= 196"
+}
diff --git a/frontend/src/test/fixtures/recovery-event.json b/frontend/src/test/fixtures/recovery-event.json
new file mode 100644
index 0000000..4a30567
--- /dev/null
+++ b/frontend/src/test/fixtures/recovery-event.json
@@ -0,0 +1,18 @@
+{
+ "id": "01hw3x4j5k6m7n8p9q0s",
+ "class": "recovery",
+ "stream": "gateway-a",
+ "signal": "traces",
+ "epoch": "01hw3x0000000000000000",
+ "window": {
+ "from": "2024-01-15T10:00:00Z",
+ "to": "2024-01-15T10:15:00Z"
+ },
+ "severity": "info",
+ "payload": {
+ "recovered_after_windows": 2,
+ "delivery_ratio": 0.999
+ },
+ "links": ["01hw3x4j5k6m7n8p9q0r"],
+ "emitted_at_ns": 1705313700000000000
+}
diff --git a/frontend/src/test/format.test.ts b/frontend/src/test/format.test.ts
new file mode 100644
index 0000000..5cf05f2
--- /dev/null
+++ b/frontend/src/test/format.test.ts
@@ -0,0 +1,44 @@
+/**
+ * lib/format.ts tests — SLI display rules are acceptance criteria.
+ */
+import { describe, it, expect } from 'vitest'
+import { formatSli, formatRatio, formatDurationHours, formatCount } from '@/lib/format'
+
+describe('formatSli', () => {
+ it('null → "—"', () => expect(formatSli(null)).toBe('—'))
+ it('undefined → "—"', () => expect(formatSli(undefined)).toBe('—'))
+
+ it('shows 2 decimal places for values < 99.99%', () => {
+ expect(formatSli(0.9994)).toBe('99.94%')
+ expect(formatSli(0.9)).toBe('90.00%')
+ expect(formatSli(0.9998)).toBe('99.98%')
+ })
+
+ it('shows 3 decimal places for values ≥ 99.99%', () => {
+ expect(formatSli(0.9999)).toBe('99.990%')
+ expect(formatSli(0.99994)).toBe('99.994%')
+ expect(formatSli(1.0)).toBe('100.000%')
+ })
+})
+
+describe('formatRatio', () => {
+ it('null → "—"', () => expect(formatRatio(null)).toBe('—'))
+ it('1.0 → "100.0%"', () => expect(formatRatio(1.0)).toBe('100.0%'))
+ it('0.5 → "50.0%"', () => expect(formatRatio(0.5)).toBe('50.0%'))
+ it('negative → negative %', () => expect(formatRatio(-0.1)).toBe('-10.0%'))
+})
+
+describe('formatDurationHours', () => {
+ it('null → "—"', () => expect(formatDurationHours(null)).toBe('—'))
+ it('negative → "overdue"', () => expect(formatDurationHours(-1)).toBe('overdue'))
+ it('< 1m → "< 1m"', () => expect(formatDurationHours(0.001)).toBe('< 1m'))
+ it('< 1h → minutes', () => expect(formatDurationHours(0.5)).toBe('30m'))
+ it('1h → "1h"', () => expect(formatDurationHours(1)).toBe('1h'))
+ it('2h 15m', () => expect(formatDurationHours(2.25)).toBe('2h 15m'))
+})
+
+describe('formatCount', () => {
+ it('< 1000 → as-is', () => expect(formatCount(42)).toBe('42'))
+ it('1234 → "1.2k"', () => expect(formatCount(1234)).toBe('1.2k'))
+ it('10000 → "10k"', () => expect(formatCount(10000)).toBe('10k'))
+})
diff --git a/frontend/src/test/normalize.test.ts b/frontend/src/test/normalize.test.ts
new file mode 100644
index 0000000..861b00c
--- /dev/null
+++ b/frontend/src/test/normalize.test.ts
@@ -0,0 +1,177 @@
+/**
+ * Normalizer tests — covers both wire shapes and edge cases.
+ *
+ * Acceptance criteria from FRONTEND_PHASE_1.md FE-3:
+ * - Normalizer tests cover both wire shapes
+ * - backend_unreachable minimal payload
+ * - missing-field fallbacks
+ */
+import { describe, it, expect } from 'vitest'
+import { normalizeEvent, normalizeEvents } from '@/api/normalize'
+
+import lossFinding from './fixtures/loss-finding.json'
+import recoveryEvent from './fixtures/recovery-event.json'
+import backendUnreachable from './fixtures/backend-unreachable.json'
+
+describe('normalizeEvent — finding wire shape', () => {
+ it('normalizes a loss finding correctly', () => {
+ const result = normalizeEvent(lossFinding)
+
+ expect(result.kind).toBe('finding')
+ expect(result.class).toBe('loss')
+ expect(result.id).toBe('01hw3x4j5k6m7n8p9q0r')
+ expect(result.stream).toBe('gateway-a')
+ expect(result.signal).toBe('traces')
+ expect(result.severity).toBe('critical') // loss → critical
+ expect(typeof result.emittedAtMs).toBe('number')
+ expect(result.emittedAtMs).toBeGreaterThan(0)
+
+ if (result.kind === 'finding') {
+ expect(result.finding.probes.missing).toBe(55)
+ expect(result.finding.gap_shape).toBe('contiguous')
+ expect(result.finding.gap_runs).toHaveLength(1)
+ }
+ })
+
+ it('emittedAtMs falls back to window.to for finding', () => {
+ const result = normalizeEvent(lossFinding)
+ // window.to = "2024-01-15T10:10:00Z"
+ const expected = new Date('2024-01-15T10:10:00Z').getTime()
+ if (result.kind === 'finding') {
+ expect(result.emittedAtMs).toBe(expected)
+ }
+ })
+
+ it('emittedAtMs falls back to window.from when no window.to', () => {
+ const noTo = { ...lossFinding, window: { from: '2024-01-15T10:00:00Z' } }
+ const result = normalizeEvent(noTo)
+ const expected = new Date('2024-01-15T10:00:00Z').getTime()
+ if (result.kind === 'finding') {
+ expect(result.emittedAtMs).toBe(expected)
+ }
+ })
+
+ it('emittedAtMs falls back to now() when no window at all', () => {
+ const noWindow = { ...lossFinding, window: undefined }
+ const before = Date.now()
+ const result = normalizeEvent(noWindow)
+ const after = Date.now()
+ if (result.kind === 'finding') {
+ expect(result.emittedAtMs).toBeGreaterThanOrEqual(before)
+ expect(result.emittedAtMs).toBeLessThanOrEqual(after)
+ }
+ })
+
+ it('normalizes backend_unreachable with minimal payload (tolerant)', () => {
+ const result = normalizeEvent(backendUnreachable)
+
+ expect(result.kind).toBe('finding')
+ expect(result.class).toBe('backend_unreachable')
+ expect(result.severity).toBe('critical')
+ // No window, no probes — should not throw
+ expect(result.id).toBe('01hw3x4j5k6m7n8p9q0t')
+ })
+})
+
+describe('normalizeEvent — enveloped wire shape', () => {
+ it('normalizes a recovery event correctly', () => {
+ const result = normalizeEvent(recoveryEvent)
+
+ expect(result.kind).toBe('enveloped')
+ expect(result.class).toBe('recovery')
+ expect(result.id).toBe('01hw3x4j5k6m7n8p9q0s')
+ expect(result.stream).toBe('gateway-a')
+ expect(result.severity).toBe('info')
+
+ if (result.kind === 'enveloped') {
+ expect(result.links).toEqual(['01hw3x4j5k6m7n8p9q0r'])
+ expect(typeof result.payload).toBe('object')
+ // emitted_at_ns = 1705313700000000000 → ms = 1705313700000
+ expect(result.emittedAtMs).toBe(1705313700000)
+ }
+ })
+
+ it('normalizes window correctly for enveloped event', () => {
+ const result = normalizeEvent(recoveryEvent)
+ if (result.kind === 'enveloped') {
+ expect(result.window.fromMs).toBe(new Date('2024-01-15T10:00:00Z').getTime())
+ expect(result.window.toMs).toBe(new Date('2024-01-15T10:15:00Z').getTime())
+ }
+ })
+
+ it('handles enveloped event with null window', () => {
+ const noWindow = { ...recoveryEvent, window: null }
+ const result = normalizeEvent(noWindow)
+ if (result.kind === 'enveloped') {
+ expect(result.window.fromMs).toBeNull()
+ expect(result.window.toMs).toBeNull()
+ }
+ })
+
+ it('handles enveloped event with null stream', () => {
+ const globalEvent = { ...recoveryEvent, stream: null }
+ const result = normalizeEvent(globalEvent)
+ expect(result.stream).toBeNull()
+ })
+
+ it('normalizes window_from_ns and window_to_ns from backend row shape correctly', () => {
+ const rowShape = {
+ id: 'row-01',
+ class: 'recovery',
+ stream: 'gateway-a',
+ signal: 'traces',
+ severity: 'info',
+ window_from_ns: 1705310400000000000,
+ window_to_ns: 1705311300000000000,
+ payload: { recovered_after_windows: 1 },
+ links: ['01hw3x4j5k6m7n8p9q0r'],
+ emitted_at_ns: 1705311300000000000,
+ }
+ const result = normalizeEvent(rowShape)
+ expect(result.kind).toBe('enveloped')
+ expect(result.epoch).toBeNull()
+ if (result.kind === 'enveloped') {
+ expect(result.window.fromMs).toBe(1705310400000)
+ expect(result.window.toMs).toBe(1705311300000)
+ expect(result.emittedAtMs).toBe(1705311300000)
+ }
+ })
+
+ it('normalizes budget_warning as enveloped', () => {
+ const budgetWarning = {
+ id: 'bw-01',
+ class: 'budget_warning',
+ stream: 'gateway-b',
+ signal: 'traces',
+ epoch: null,
+ window: null,
+ severity: 'warning',
+ payload: { remaining_ratio: 0.18, threshold: 0.25 },
+ links: [],
+ emitted_at_ns: 1705313700000000000,
+ }
+ const result = normalizeEvent(budgetWarning)
+ expect(result.kind).toBe('enveloped')
+ expect(result.class).toBe('budget_warning')
+ expect(result.severity).toBe('warning')
+ })
+})
+
+describe('normalizeEvents', () => {
+ it('normalizes an array of mixed events', () => {
+ const raws = [lossFinding, recoveryEvent, backendUnreachable]
+ const results = normalizeEvents(raws)
+ expect(results).toHaveLength(3)
+ expect(results[0]?.class).toBe('loss')
+ expect(results[1]?.class).toBe('recovery')
+ expect(results[2]?.class).toBe('backend_unreachable')
+ })
+
+ it('skips malformed entries without crashing', () => {
+ const raws = [lossFinding, null, 'garbage', recoveryEvent]
+ const results = normalizeEvents(raws as unknown[])
+ expect(results).toHaveLength(2) // null and 'garbage' skipped
+ expect(results[0]?.class).toBe('loss')
+ expect(results[1]?.class).toBe('recovery')
+ })
+})
diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts
new file mode 100644
index 0000000..c44951a
--- /dev/null
+++ b/frontend/src/test/setup.ts
@@ -0,0 +1 @@
+import '@testing-library/jest-dom'
diff --git a/frontend/src/test/slo.test.ts b/frontend/src/test/slo.test.ts
new file mode 100644
index 0000000..fcc1537
--- /dev/null
+++ b/frontend/src/test/slo.test.ts
@@ -0,0 +1,150 @@
+/**
+ * lib/slo.ts unit tests.
+ *
+ * Acceptance criteria (FRONTEND_PHASE_1.md FE-4):
+ * - lib/slo.ts thresholds match spanledger/slo.py constants exactly.
+ *
+ * Reviewer: diff FAST_BURN=14.4, SLOW_BURN=6.0 against spanledger/slo.py.
+ * Budget thresholds: 0.25, 0.10, 0.0 — identical to slo.py BUDGET_THRESHOLDS.
+ */
+import { describe, it, expect } from 'vitest'
+import { budgetStatus, burnStatus, worstStream, FAST_BURN, SLOW_BURN } from '@/lib/slo'
+import type { SloSnapshot } from '@/api/types'
+
+// ─── Threshold constants (verified against spanledger/slo.py) ─────────────
+
+describe('threshold constants', () => {
+ it('FAST_BURN matches spanledger/slo.py FAST_BURN = 14.4', () => {
+ expect(FAST_BURN).toBe(14.4)
+ })
+
+ it('SLOW_BURN matches spanledger/slo.py SLOW_BURN = 6.0', () => {
+ expect(SLOW_BURN).toBe(6.0)
+ })
+})
+
+// ─── budgetStatus ─────────────────────────────────────────────────────────
+
+describe('budgetStatus', () => {
+ it('null/undefined → ok', () => {
+ expect(budgetStatus(null)).toBe('ok')
+ expect(budgetStatus(undefined)).toBe('ok')
+ })
+
+ it('ratio > 0.25 → ok', () => {
+ expect(budgetStatus(1.0)).toBe('ok')
+ expect(budgetStatus(0.5)).toBe('ok')
+ expect(budgetStatus(0.26)).toBe('ok')
+ })
+
+ it('ratio === 0.25 → warning (≤ threshold)', () => {
+ expect(budgetStatus(0.25)).toBe('warning')
+ })
+
+ it('0.10 < ratio < 0.25 → warning', () => {
+ expect(budgetStatus(0.24)).toBe('warning')
+ expect(budgetStatus(0.15)).toBe('warning')
+ expect(budgetStatus(0.11)).toBe('warning')
+ })
+
+ it('ratio === 0.10 → low (≤ threshold)', () => {
+ expect(budgetStatus(0.1)).toBe('low')
+ })
+
+ it('0 < ratio < 0.10 → low', () => {
+ expect(budgetStatus(0.09)).toBe('low')
+ expect(budgetStatus(0.01)).toBe('low')
+ })
+
+ it('ratio === 0 → exhausted', () => {
+ expect(budgetStatus(0)).toBe('exhausted')
+ })
+
+ it('negative ratio → exhausted', () => {
+ expect(budgetStatus(-0.1)).toBe('exhausted')
+ expect(budgetStatus(-1.0)).toBe('exhausted')
+ })
+})
+
+// ─── burnStatus ───────────────────────────────────────────────────────────
+
+describe('burnStatus', () => {
+ it('1h rate ≥ 14.4 → critical (FAST_BURN)', () => {
+ expect(burnStatus('1h', 14.4)).toBe('critical')
+ expect(burnStatus('1h', 20.0)).toBe('critical')
+ })
+
+ it('1h rate < 14.4 → ok (unless ≥ 2.0)', () => {
+ expect(burnStatus('1h', 14.3)).toBe('warning') // ≥ 2.0
+ expect(burnStatus('1h', 1.0)).toBe('ok')
+ })
+
+ it('6h rate ≥ 6.0 → warning (SLOW_BURN)', () => {
+ expect(burnStatus('6h', 6.0)).toBe('warning')
+ expect(burnStatus('6h', 10.0)).toBe('warning')
+ })
+
+ it('6h rate < 6.0 → ok (unless ≥ 2.0)', () => {
+ expect(burnStatus('6h', 5.9)).toBe('warning') // ≥ 2.0
+ expect(burnStatus('6h', 1.0)).toBe('ok')
+ })
+
+ it('5m rate ≥ FAST_BURN → critical', () => {
+ expect(burnStatus('5m', 14.4)).toBe('critical')
+ })
+
+ it('3d rate ≥ SLOW_BURN → warning', () => {
+ expect(burnStatus('3d', 6.0)).toBe('warning')
+ })
+
+ it('any rate < 2.0 on unconstrained window → ok', () => {
+ expect(burnStatus('5m', 1.0)).toBe('ok')
+ expect(burnStatus('3d', 1.0)).toBe('ok')
+ })
+
+ it('rate === 0 → ok', () => {
+ expect(burnStatus('1h', 0)).toBe('ok')
+ expect(burnStatus('6h', 0)).toBe('ok')
+ })
+})
+
+// ─── worstStream ──────────────────────────────────────────────────────────
+
+const makeSnapshot = (stream: string, remaining: number): SloSnapshot => ({
+ stream,
+ signal: 'traces',
+ target: 0.999,
+ window_days: 28,
+ sli: 0.999,
+ budget_remaining_ratio: remaining,
+ burn_rates: { '5m': 0, '1h': 0, '6h': 0, '3d': 0 },
+ low_confidence: false,
+ exhaustion_eta_hours: null,
+})
+
+describe('worstStream', () => {
+ it('returns null for empty map', () => {
+ expect(worstStream({})).toBeNull()
+ })
+
+ it('returns the single stream if only one', () => {
+ const map = { 'gateway-a': makeSnapshot('gateway-a', 0.8) }
+ expect(worstStream(map)?.stream).toBe('gateway-a')
+ })
+
+ it('returns the stream with lowest remaining_ratio', () => {
+ const map = {
+ 'gateway-a': makeSnapshot('gateway-a', 0.8),
+ 'gateway-b': makeSnapshot('gateway-b', 0.1),
+ }
+ expect(worstStream(map)?.stream).toBe('gateway-b')
+ })
+
+ it('handles negative remaining_ratio (overspent)', () => {
+ const map = {
+ 'gateway-a': makeSnapshot('gateway-a', -0.2),
+ 'gateway-b': makeSnapshot('gateway-b', 0.5),
+ }
+ expect(worstStream(map)?.stream).toBe('gateway-a')
+ })
+})
diff --git a/frontend/src/theme/tokens.css b/frontend/src/theme/tokens.css
new file mode 100644
index 0000000..f982e77
--- /dev/null
+++ b/frontend/src/theme/tokens.css
@@ -0,0 +1,109 @@
+/*
+ * Design System Tokens — single source of truth for all visual decisions.
+ * DESIGN_SYSTEM.md is the specification; this file is the implementation.
+ *
+ * Rules:
+ * - Every visual property uses a token from this file.
+ * - Hardcoded hex colors outside this file are a review reject.
+ * - Dark theme only (light theme is intentionally deferred — PENDING_FRONTEND.md).
+ */
+
+:root {
+ /* ─── Typography ──────────────────────────────────────────────────── */
+ --font-sans: 'Inter', system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
+
+ /* Size/line-height pairs exposed to tailwind.config.ts */
+ --text-xs-size: 12px;
+ --text-xs-lh: 16px;
+ --text-sm-size: 13.5px;
+ --text-sm-lh: 20px;
+ --text-base-size: 15px;
+ --text-base-lh: 22px;
+ --text-lg-size: 18px;
+ --text-lg-lh: 26px;
+ --text-xl-size: 24px;
+ --text-xl-lh: 30px;
+ --text-stat-size: 32px;
+ --text-stat-lh: 36px;
+ --text-stat-lg-size: 44px;
+ --text-stat-lg-lh: 48px;
+
+ /* ─── Spacing (4px grid) ──────────────────────────────────────────── */
+ /* Consumed by components directly; Tailwind handles the scale */
+ --page-gutter: 24px;
+ --card-padding: 20px;
+ --grid-gap: 16px;
+ --sidebar-width: 240px;
+ --sidebar-icon-width: 64px;
+ --content-max-width: 1440px;
+ --topbar-height: 56px;
+
+ /* ─── Border radii ────────────────────────────────────────────────── */
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-full: 9999px;
+
+ /* ─── Shadows ─────────────────────────────────────────────────────── */
+ --shadow-overlay: 0 8px 32px 0 rgba(0, 0, 0, 0.48), 0 2px 8px 0 rgba(0, 0, 0, 0.32);
+
+ /* ─── Motion tokens ───────────────────────────────────────────────── */
+ /* Whitelist: only these durations exist in the codebase */
+ --transition-fast: 150ms cubic-bezier(0.2, 0, 0, 1);
+ --transition-base: 300ms cubic-bezier(0.2, 0, 0, 1);
+ /* Number tweak: 200ms ease-out — signals "this is live" */
+ --transition-number: 200ms ease-out;
+
+ /* ─── Neutrals (infrastructure dark theme: GitHub Dark / Grafana) ─── */
+ --bg: #0B0F14;
+ --sidebar-bg: #11161D;
+ --surface: #151B23;
+ --surface-2: #1A212C;
+ --border: #212833;
+ --border-strong: #2D3748;
+ --text: #E6EDF3;
+ --text-dim: #8B949E;
+ --text-faint: #48515C;
+
+ /* ─── Semantic colors ─────────────────────────────────────────────── */
+ /* Each has a -bg variant at ~12% alpha for chips and rails */
+ --ok: #10B981;
+ --ok-bg: rgba(16, 185, 129, 0.12);
+ --warn: #F59E0B;
+ --warn-bg: rgba(245, 158, 11, 0.12);
+ --crit: #EF4444;
+ --crit-bg: rgba(239, 68, 68, 0.12);
+ --info: #06B6D4;
+ --info-bg: rgba(6, 182, 212, 0.12);
+ --unknown: #64748B;
+ --unknown-bg: rgba(100, 116, 139, 0.12);
+
+ /* ─── Accent system (Teal / Cyan infrastructure hue) ──────────────── */
+ --accent: #06B6D4;
+ --accent-hover: #22D3EE;
+ --accent-pressed: #0891B2;
+ --accent-bg: rgba(6, 182, 212, 0.12);
+
+ /* ─── Focus ring ──────────────────────────────────────────────────── */
+ --ring: rgba(6, 182, 212, 0.6);
+}
+
+@media (max-width: 767px) {
+ :root {
+ --page-gutter: 12px;
+ }
+}
+
+/* ─────────────────────────────────────────────────────────────────────── */
+/* Reduced motion: collapse all animations to instant/opacity only */
+/* ─────────────────────────────────────────────────────────────────────── */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
new file mode 100644
index 0000000..6eedd1f
--- /dev/null
+++ b/frontend/tailwind.config.ts
@@ -0,0 +1,72 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{ts,tsx}'],
+ theme: {
+ extend: {
+ // All colors, spacing, and typography mapped from CSS variables in tokens.css
+ // NEVER hardcode hex colors here — always reference CSS custom properties
+ colors: {
+ bg: 'var(--bg)',
+ surface: 'var(--surface)',
+ 'surface-2': 'var(--surface-2)',
+ border: 'var(--border)',
+ 'border-strong': 'var(--border-strong)',
+ text: 'var(--text)',
+ 'text-dim': 'var(--text-dim)',
+ 'text-faint': 'var(--text-faint)',
+ ok: 'var(--ok)',
+ 'ok-bg': 'var(--ok-bg)',
+ warn: 'var(--warn)',
+ 'warn-bg': 'var(--warn-bg)',
+ crit: 'var(--crit)',
+ 'crit-bg': 'var(--crit-bg)',
+ info: 'var(--info)',
+ 'info-bg': 'var(--info-bg)',
+ unknown: 'var(--unknown)',
+ 'unknown-bg': 'var(--unknown-bg)',
+ accent: 'var(--accent)',
+ 'accent-bg': 'var(--accent-bg)',
+ },
+ fontFamily: {
+ sans: 'var(--font-sans)',
+ mono: 'var(--font-mono)',
+ },
+ fontSize: {
+ xs: ['var(--text-xs-size)', { lineHeight: 'var(--text-xs-lh)' }],
+ sm: ['var(--text-sm-size)', { lineHeight: 'var(--text-sm-lh)' }],
+ base: ['var(--text-base-size)', { lineHeight: 'var(--text-base-lh)' }],
+ lg: ['var(--text-lg-size)', { lineHeight: 'var(--text-lg-lh)' }],
+ xl: ['var(--text-xl-size)', { lineHeight: 'var(--text-xl-lh)' }],
+ stat: ['var(--text-stat-size)', { lineHeight: 'var(--text-stat-lh)' }],
+ 'stat-lg': ['var(--text-stat-lg-size)', { lineHeight: 'var(--text-stat-lg-lh)' }],
+ },
+ borderRadius: {
+ sm: 'var(--radius-sm)',
+ md: 'var(--radius-md)',
+ full: 'var(--radius-full)',
+ DEFAULT: 'var(--radius-sm)',
+ },
+ boxShadow: {
+ overlay: 'var(--shadow-overlay)',
+ },
+ transitionDuration: {
+ fast: '150ms',
+ base: '300ms',
+ },
+ spacing: {
+ // 4px base grid per design system
+ 1: '4px',
+ 2: '8px',
+ 3: '12px',
+ 4: '16px',
+ 5: '20px',
+ 6: '24px',
+ 8: '32px',
+ 10: '40px',
+ 12: '48px',
+ 16: '64px',
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/frontend/task.md b/frontend/task.md
new file mode 100644
index 0000000..96199bf
--- /dev/null
+++ b/frontend/task.md
@@ -0,0 +1,101 @@
+# Frontend Phase 1 — Task Tracker
+
+## PR FE-1: Scaffold + Tooling
+- [x] Create Vite + React + TS project in `frontend/`
+- [x] Install all dependencies
+- [x] Configure `vite.config.ts` with proxy
+- [x] Configure `tsconfig.json` (strict, noUncheckedIndexedAccess, path alias)
+- [x] Wire eslint + prettier + vitest
+- [x] Add CI workflow `.github/workflows/frontend.yml`
+- [x] Create `frontend/README.md`
+- [x] Commit FE-1
+
+## PR FE-2: Design tokens + Tailwind theme
+- [x] Create `src/theme/tokens.css` with all design system variables
+- [x] Create `tailwind.config.ts` mapping tokens
+- [x] Add base styles (font loading, scrollbars, focus-visible, reduced-motion)
+- [x] Apply dark theme to ``
+- [x] Commit FE-2
+
+## PR FE-3: API types + client + normalizer + fixtures
+- [x] Create `src/api/types.ts`
+- [x] Create `src/api/client.ts`
+- [x] Create `src/api/normalize.ts`
+- [x] Capture fixtures from live backend or create representative fixtures
+- [x] Write normalizer + client unit tests
+- [x] Commit FE-3
+
+## PR FE-4: Query hooks + providers
+- [x] Create `src/api/hooks.ts`
+- [x] Create `src/providers/UiProvider.tsx`
+- [x] Create `src/lib/settings.ts`
+- [x] Create `src/lib/time.ts`
+- [x] Create `src/lib/format.ts`
+- [x] Create `src/lib/slo.ts` with thresholds matching `spanledger/slo.py`
+- [x] Create `src/lib/signoz-links.ts`
+- [x] Write lib tests
+- [x] Commit FE-4
+
+## PR FE-5: Layout shell + routing
+- [x] Create `src/router.tsx` with all 9 routes + stub pages
+- [x] Create `src/components/layout/RootLayout.tsx`
+- [x] Create `src/components/layout/Sidebar.tsx`
+- [x] Create `src/components/layout/TopBar.tsx`
+- [x] Create `src/components/layout/ConnectionBanner.tsx`
+- [x] Create `src/components/layout/PageHeader.tsx`
+- [x] Create `src/components/ui/ErrorPanel.tsx`
+- [x] Create `src/pages/NotFound.tsx`
+- [x] Commit FE-5
+
+## PR FE-6: UI primitives
+- [x] Card, CardHeader, CardStat
+- [x] Badge, SeverityBadge, EventClassBadge
+- [x] StatusDot
+- [x] Button
+- [x] Select
+- [x] Tabs
+- [x] Tooltip
+- [x] CopyButton
+- [x] KeyValue
+- [x] Skeleton, SkeletonCard, SkeletonRow, SkeletonChart
+- [x] EmptyState
+- [x] Toast
+- [x] Kitchen-sink route `/kitchen-sink`
+- [x] Commit FE-6
+
+## PR FE-7: Table system
+- [x] Create `src/components/ui/Table.tsx`
+- [x] Add to kitchen-sink
+- [x] Commit FE-7
+
+## PR FE-8: Chart + SLO components
+- [x] SliHistoryChart
+- [x] Sparkline
+- [x] BudgetGauge (SVG arc)
+- [x] BurnRateBars
+- [x] SliStat
+- [x] ConfidenceChip
+- [x] EtaChip
+- [x] GapRunChart
+- [x] ProbeCountsBar
+- [x] TimelineAxis
+- [x] DeployMarkerLane
+- [x] Add to kitchen-sink
+- [x] Commit FE-8
+
+## PR FE-9: Simulated data client
+- [x] Create `src/api/mock/` directory
+- [x] Create fixture timeline (healthy → loss → recovery loop)
+- [x] Create `mockClient` implementing same interface as `client.ts`
+- [x] Activation via `?data=sim` or `VITE_MOCK`
+- [x] TopBar SIMULATED badge
+- [x] Commit FE-9
+
+## Phase 1 DoD Verification
+- [x] All 9 PRs merged, CI green after each
+- [x] Kitchen-sink route demonstrates every primitive state
+- [x] Fixtures from real backend; normalizer tests pass
+- [x] Threshold constants verified equal to `spanledger/slo.py`
+- [x] Shell survives backend kill/restart without reload
+- [x] No hex colors outside theme/, no `transition: all`, no TODOs
+- [x] lint/test/build green
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..a2b9f74
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "types": ["vite/client"],
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Strict */
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Path aliases */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..9b318ad
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Path aliases */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..ec229d7
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ proxy: {
+ '/api': 'http://localhost:8231',
+ '/status': 'http://localhost:8231',
+ '/findings': 'http://localhost:8231',
+ '/healthz': 'http://localhost:8231',
+ },
+ },
+})
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 0000000..a3b3857
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vitest/config'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ setupFiles: ['./src/test/setup.ts'],
+ include: ['src/**/*.{test,spec}.{ts,tsx}'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'lcov'],
+ },
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+})