diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index af1725dad..321787263 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -33,3 +33,37 @@ jobs:
- name: Run full test suite
run: python -m pytest -q
+
+ frontend:
+ name: Frontend lint, test, build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7
+ with:
+ persist-credentials: false
+
+ - name: Set up Node
+ uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # actions/setup-node@v5
+ with:
+ # Matches frontend/mise.toml's pin (setup-node doesn't parse mise.toml).
+ node-version: "24"
+
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Install dependencies
+ working-directory: frontend
+ run: pnpm install --frozen-lockfile
+
+ - name: Lint
+ working-directory: frontend
+ run: pnpm run lint
+
+ - name: Test
+ working-directory: frontend
+ run: pnpm run test
+
+ - name: Build
+ working-directory: frontend
+ run: pnpm run build
diff --git a/AGENTS.md b/AGENTS.md
index 3848e9cf0..d43bdd733 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -64,6 +64,15 @@ in the same spirit) -- never against real data, per the hard rule above.
against a live local stack (`make up`) and self-skip without one -- see
[README.md](README.md#local-product-stack-docker-compose).
+`frontend/` has its own toolchain (Node pinned via `frontend/mise.toml`,
+pnpm via Corepack -- do not add a second Node package manager or a
+floating Node version):
+
+```bash
+cd frontend && pnpm install
+pnpm run lint && pnpm run test && pnpm run build
+```
+
## CI gates
`.github/workflows/tests.yml` runs the full suite on every PR to `main`.
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 081d94aa7..7200368ba 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -164,3 +164,27 @@ allow/deny ABAC boundary against a throwaway migrated Postgres database
excluded from the list and 403s on direct fetch), and proves a forged
token is rejected. `scripts/seed_demo_data.py` populates the docker-compose
stack itself with the same shape of synthetic data for manual/frontend use.
+`CORSMiddleware` (`backend/app/main.py`) allows exactly the frontend's
+origin(s) (`FRONTEND_ORIGINS`), `GET` only, `Authorization` header only.
+
+### Frontend (`frontend/`)
+
+React + Vite + TypeScript, pinned Node via `mise.toml`, pnpm via Corepack.
+`react-oidc-context` drives a real Authorization Code redirect through
+Keycloak (`src/main.tsx`'s `AuthProvider`) -- no mocked auth, no static
+HTML. `src/api.ts` calls the FastAPI backend directly with the token
+Keycloak issued; `src/App.tsx` renders the post list and a detail popup
+(the Figma frame `SBpgot7uTvMxEaxUwvoc0S` attachment point -- Event
+Lineage / Keyman / Knowledge Graph / LLM chat panels land in Phase 2-4).
+Served in `docker compose` via a two-stage build (`frontend/Dockerfile`):
+`pnpm run build` then `nginx` serving the static bundle, with `VITE_*`
+config baked in at build time from the same `.env` ports every other
+service uses (Vite embeds `import.meta.env.VITE_*` at build time, not
+runtime, so these are Docker build args, not container env vars).
+`src/App.test.tsx` mocks `react-oidc-context`'s `useAuth` to test the
+component's own render logic (login button -> `signinRedirect()`; fetch
+posts with the token -> render list -> click -> popup shows the fetched
+body) -- the real OIDC cryptography is proven elsewhere
+(`scripts/smoke_test_oidc.py`, `backend/tests/test_api.py`), so this test
+isn't re-proving that, only that the UI wires the pieces together
+correctly.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 291600753..72e784ddf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,39 @@ All notable changes to this project are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.7.0] - 2026-08-13
+
+### Added
+
+- `frontend/`: React + Vite + TypeScript, pinned Node via `mise.toml`,
+ pnpm via Corepack -- a real client, not mocked and not static HTML.
+ `react-oidc-context` drives an actual Authorization Code redirect
+ through Keycloak; the post list and detail popup call the FastAPI
+ backend over real `fetch()` with the token Keycloak issued.
+ `src/App.test.tsx` covers the login-redirect and fetch-then-render
+ paths (`useAuth` mocked -- the real OIDC round-trip is proven
+ elsewhere, by `scripts/smoke_test_oidc.py` and `backend/tests/test_api.py`).
+- `frontend/Dockerfile` + `nginx.conf`: two-stage build (`pnpm run build`
+ then nginx serving the static bundle) added as docker-compose's fourth
+ service, `VITE_*` config baked in at build time from the same `.env`
+ ports every other service uses. Both stages pin the base image by
+ digest; the runtime stage declares `USER nginx` and listens on 8080
+ so the master process does not need root to bind a port.
+- `backend/app/main.py`: `CORSMiddleware`, scoped to exactly the
+ frontend's origin(s) (`FRONTEND_ORIGINS`), `GET` only, `Authorization`
+ header only -- verified with a real cross-origin preflight + GET against
+ the live stack, not just unit-tested in isolation.
+- `.github/workflows/tests.yml`: added a `frontend` job (lint, test,
+ build) alongside the existing Python `pytest` job.
+
+### Fixed
+
+- Keycloak's `lineageweave-frontend` client (`docker/keycloak/realm-export.json`)
+ now allows both the Vite dev-server origin (`:5173`) and the
+ docker-compose-served frontend's origin (`:15173`) as redirect URIs and
+ web origins -- the login redirect only worked from one of the two
+ before this.
+
## [0.6.0] - 2026-08-13
### Added
diff --git a/README.md b/README.md
index 1ba9757ad..6371689a3 100644
--- a/README.md
+++ b/README.md
@@ -149,6 +149,28 @@ and the deny path against a live Keycloak + throwaway Postgres database,
including that a private post scoped to a *different* corporate entity is
excluded from the list and 403s on direct fetch.
+`frontend/` (React + Vite + TypeScript, `docker compose`'s fourth service)
+is a real client, not mocked or static: `react-oidc-context` drives an
+actual Authorization Code redirect through Keycloak, and the post list /
+detail popup call the FastAPI backend over real `fetch()` with the token
+Keycloak issued.
+
+```bash
+make up
+make seed
+cd frontend && cp .env.example .env.local && pnpm install && pnpm run dev
+# -> http://localhost:5173, click "Log in", redirects through the real
+# Keycloak login page for demo.analyst / lineageweave-demo-only
+```
+
+`docker compose up` also builds and serves the frontend itself (nginx,
+`frontend/Dockerfile`) at `http://localhost:15173` -- the `VITE_*` build
+args are wired from the same `.env` ports as every other service.
+`frontend/src/App.test.tsx` covers the login-redirect and
+fetch-then-render-popup paths (`react-oidc-context`'s `useAuth` mocked --
+the *real* OIDC round-trip is what `scripts/smoke_test_oidc.py` and
+`backend/tests/test_api.py` already prove against a live Keycloak).
+
## Modular / standalone
This repo runs standalone (own server, own tests, own CI) and is equally
diff --git a/backend/app/config.py b/backend/app/config.py
index 562892d5b..7a753ed9c 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -25,6 +25,9 @@ class Settings:
# docker-compose the two differ (internal DNS name vs. the
# host-published port a browser actually hits).
keycloak_issuer: str
+ # Exact browser origins allowed by CORS. Comma-separated FRONTEND_ORIGINS;
+ # never a wildcard -- the backend only serves the product UI.
+ frontend_origins: list[str]
@property
def keycloak_jwks_uri(self) -> str:
@@ -33,7 +36,6 @@ def keycloak_jwks_uri(self) -> str:
def load_settings() -> Settings:
- """Read Settings from the environment, with local-dev defaults only."""
"""Read Settings from the environment, with local-dev defaults only."""
keycloak_base_url = os.environ.get("KEYCLOAK_BASE_URL", "http://localhost:18080")
keycloak_realm = os.environ.get("KEYCLOAK_REALM", "lineageweave-demo")
@@ -48,4 +50,9 @@ def load_settings() -> Settings:
keycloak_issuer=os.environ.get(
"KEYCLOAK_ISSUER", f"{keycloak_base_url}/realms/{keycloak_realm}"
),
+ frontend_origins=[
+ origin.strip()
+ for origin in os.environ.get("FRONTEND_ORIGINS", "http://localhost:5173").split(",")
+ if origin.strip()
+ ],
)
diff --git a/backend/app/main.py b/backend/app/main.py
index ab0462feb..10b4cb201 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -24,6 +24,7 @@
import asyncpg
from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.middleware.cors import CORSMiddleware
from backend.app.auth import CurrentAccount, get_current_account
from backend.app.config import load_settings
@@ -44,6 +45,12 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="LineageWeave API", lifespan=lifespan)
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=load_settings().frontend_origins,
+ allow_methods=["GET"],
+ allow_headers=["Authorization"],
+)
def _require_post_read(account: CurrentAccount) -> None:
diff --git a/backend/tests/test_config.py b/backend/tests/test_config.py
new file mode 100644
index 000000000..655f51654
--- /dev/null
+++ b/backend/tests/test_config.py
@@ -0,0 +1,23 @@
+"""Settings parsing tests that do not need a live Postgres or Keycloak."""
+
+from __future__ import annotations
+
+from backend.app.config import load_settings
+
+
+def test_frontend_origins_are_parsed_from_comma_separated_env(monkeypatch) -> None:
+ """CORS allow-list is an explicit env CSV, never a wildcard default."""
+ monkeypatch.setenv(
+ "FRONTEND_ORIGINS",
+ "http://localhost:5173, http://localhost:15173",
+ )
+ settings = load_settings()
+ assert settings.frontend_origins == [
+ "http://localhost:5173",
+ "http://localhost:15173",
+ ]
+
+
+def test_frontend_origins_drop_blank_entries(monkeypatch) -> None:
+ monkeypatch.setenv("FRONTEND_ORIGINS", "http://localhost:5173,,")
+ assert load_settings().frontend_origins == ["http://localhost:5173"]
diff --git a/docker-compose.yml b/docker-compose.yml
index de3ca1ce9..2f5d471ce 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -78,6 +78,7 @@ services:
KEYCLOAK_ISSUER: http://localhost:${KEYCLOAK_PORT:-18080}/realms/lineageweave-demo
KEYCLOAK_REALM: lineageweave-demo
KEYCLOAK_CLIENT_ID: lineageweave-frontend
+ FRONTEND_ORIGINS: http://localhost:${FRONTEND_PORT:-15173}
ports:
- "${BACKEND_PORT:-18420}:8000"
depends_on:
@@ -86,6 +87,18 @@ services:
keycloak:
condition: service_started
+ frontend:
+ build:
+ context: ./frontend
+ args:
+ VITE_KEYCLOAK_ISSUER: http://localhost:${KEYCLOAK_PORT:-18080}/realms/lineageweave-demo
+ VITE_KEYCLOAK_CLIENT_ID: lineageweave-frontend
+ VITE_BACKEND_BASE_URL: http://localhost:${BACKEND_PORT:-18420}
+ ports:
+ - "${FRONTEND_PORT:-15173}:8080"
+ depends_on:
+ - backend
+
volumes:
postgres_data:
valkey_data:
diff --git a/docker/keycloak/realm-export.json b/docker/keycloak/realm-export.json
index ca6ce76f4..5e12e3fd3 100644
--- a/docker/keycloak/realm-export.json
+++ b/docker/keycloak/realm-export.json
@@ -20,8 +20,8 @@
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
- "redirectUris": ["http://localhost:5173/*"],
- "webOrigins": ["http://localhost:5173"],
+ "redirectUris": ["http://localhost:5173/*", "http://localhost:15173/*"],
+ "webOrigins": ["http://localhost:5173", "http://localhost:15173"],
"protocolMappers": [
{
"name": "corp-code",
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 000000000..cae190249
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1,5 @@
+# Copy to .env.local to override. Defaults already match this repo's
+# docker-compose.yml (see ../.env.example).
+VITE_KEYCLOAK_ISSUER=http://localhost:18080/realms/lineageweave-demo
+VITE_KEYCLOAK_CLIENT_ID=lineageweave-frontend
+VITE_BACKEND_BASE_URL=http://localhost:18420
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /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 000000000..6fa991dad
--- /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/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 000000000..aa2682a81
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,27 @@
+FROM node:24-slim@sha256:3638d9a6fe4030bd716be989438248074489337ba3275657f93595428be4fc03 AS build
+WORKDIR /app
+RUN corepack enable
+COPY package.json pnpm-lock.yaml ./
+RUN pnpm install --frozen-lockfile
+COPY . .
+# Vite bakes VITE_* vars in at build time, not runtime -- build args let
+# docker-compose pass through its own (possibly operator-overridden) ports.
+ARG VITE_KEYCLOAK_ISSUER
+ARG VITE_KEYCLOAK_CLIENT_ID
+ARG VITE_BACKEND_BASE_URL
+ENV VITE_KEYCLOAK_ISSUER=${VITE_KEYCLOAK_ISSUER} \
+ VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID} \
+ VITE_BACKEND_BASE_URL=${VITE_BACKEND_BASE_URL}
+RUN pnpm run build
+
+FROM nginx:1.27-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
+COPY --from=build /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+# Official nginx binds :80 as root and writes /var/run/nginx.pid. Move
+# both so the declared non-root USER can actually start the master
+# process (DS-0002). Compose publishes this 8080, not 80.
+RUN sed -i 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf \
+ && sed -i '/^user /d' /etc/nginx/nginx.conf \
+ && chown -R nginx:nginx /usr/share/nginx/html /var/cache/nginx /var/log/nginx
+USER nginx
+EXPOSE 8080
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 000000000..d6af7e39f
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,32 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the Oxlint configuration
+
+If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
+
+```json
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "options": {
+ "typeAware": true
+ },
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
+```
+
+See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 000000000..884ca64d0
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ LineageWeave
+
+
+
+
+
+
diff --git a/frontend/mise.toml b/frontend/mise.toml
new file mode 100644
index 000000000..6ea5a7e1f
--- /dev/null
+++ b/frontend/mise.toml
@@ -0,0 +1,2 @@
+[tools]
+node = "24"
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 000000000..52d1b7cea
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,12 @@
+server {
+ listen 8080;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # SPA fallback: react-oidc-context's redirect_uri is the app root, and
+ # client-side routing (added in a later phase) needs every path to
+ # resolve to index.html.
+ location / {
+ try_files $uri /index.html;
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 000000000..9e9885691
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.7.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "oxlint",
+ "preview": "vite preview",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "oidc-client-ts": "^3.5.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "react-oidc-context": "^3.3.1"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^7.0.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.4",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.4",
+ "jsdom": "^30.0.1",
+ "oxlint": "^1.75.0",
+ "typescript": "~6.0.2",
+ "vite": "^8.2.0",
+ "vitest": "^4.1.10"
+ },
+ "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
+}
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
new file mode 100644
index 000000000..d82b64db1
--- /dev/null
+++ b/frontend/pnpm-lock.yaml
@@ -0,0 +1,1568 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ oidc-client-ts:
+ specifier: ^3.5.0
+ version: 3.5.0
+ react:
+ specifier: ^19.2.8
+ version: 19.2.8
+ react-dom:
+ specifier: ^19.2.8
+ version: 19.2.8(react@19.2.8)
+ react-oidc-context:
+ specifier: ^3.3.1
+ version: 3.3.1(oidc-client-ts@3.5.0)(react@19.2.8)
+ devDependencies:
+ '@testing-library/jest-dom':
+ specifier: ^7.0.1
+ version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.1(@types/node@24.13.3)))
+ '@testing-library/react':
+ specifier: ^16.3.2
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@testing-library/user-event':
+ specifier: ^14.6.4
+ version: 14.6.4(@testing-library/dom@10.4.1)
+ '@types/node':
+ specifier: ^24.13.3
+ version: 24.13.3
+ '@types/react':
+ specifier: ^19.2.17
+ version: 19.2.18
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.4(@types/react@19.2.18)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.4
+ version: 6.0.5(vite@8.2.1(@types/node@24.13.3))
+ jsdom:
+ specifier: ^30.0.1
+ version: 30.0.1
+ oxlint:
+ specifier: ^1.75.0
+ version: 1.78.0
+ typescript:
+ specifier: ~6.0.2
+ version: 6.0.3
+ vite:
+ specifier: ^8.2.0
+ version: 8.2.1(@types/node@24.13.3)
+ vitest:
+ specifier: ^4.1.10
+ version: 4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.1(@types/node@24.13.3))
+
+packages:
+
+ '@adobe/css-tools@4.5.0':
+ resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
+
+ '@asamuzakjp/css-color@6.0.7':
+ resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==}
+ engines: {node: ^22.13.0 || >=24.0.0}
+
+ '@asamuzakjp/dom-selector@8.3.2':
+ resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==}
+ engines: {node: ^22.13.0 || >=24.0.0}
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
+ '@bramus/specificity@2.4.2':
+ resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
+ hasBin: true
+
+ '@csstools/color-helpers@6.1.0':
+ resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==}
+ engines: {node: '>=20.19.0'}
+
+ '@csstools/css-calc@3.3.0':
+ resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-color-parser@4.1.10':
+ resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0':
+ resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.7':
+ resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==}
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+
+ '@csstools/css-tokenizer@4.0.0':
+ resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
+ engines: {node: '>=20.19.0'}
+
+ '@exodus/bytes@1.15.1':
+ resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ '@noble/hashes': ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@noble/hashes':
+ optional: true
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@oxc-project/types@0.144.0':
+ resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==}
+
+ '@oxlint/binding-android-arm-eabi@1.78.0':
+ resolution: {integrity: sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxlint/binding-android-arm64@1.78.0':
+ resolution: {integrity: sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxlint/binding-darwin-arm64@1.78.0':
+ resolution: {integrity: sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint/binding-darwin-x64@1.78.0':
+ resolution: {integrity: sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint/binding-freebsd-x64@1.78.0':
+ resolution: {integrity: sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.78.0':
+ resolution: {integrity: sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm-musleabihf@1.78.0':
+ resolution: {integrity: sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-gnu@1.78.0':
+ resolution: {integrity: sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-musl@1.78.0':
+ resolution: {integrity: sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-ppc64-gnu@1.78.0':
+ resolution: {integrity: sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-gnu@1.78.0':
+ resolution: {integrity: sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-musl@1.78.0':
+ resolution: {integrity: sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-s390x-gnu@1.78.0':
+ resolution: {integrity: sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-gnu@1.78.0':
+ resolution: {integrity: sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-musl@1.78.0':
+ resolution: {integrity: sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-openharmony-arm64@1.78.0':
+ resolution: {integrity: sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxlint/binding-win32-arm64-msvc@1.78.0':
+ resolution: {integrity: sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint/binding-win32-ia32-msvc@1.78.0':
+ resolution: {integrity: sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxlint/binding-win32-x64-msvc@1.78.0':
+ resolution: {integrity: sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/binding-android-arm64@1.2.4':
+ resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.2.4':
+ resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.4':
+ resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.4':
+ resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.2.4':
+ resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.4':
+ resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.4':
+ resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.2.4':
+ resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.2.4':
+ resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.2.4':
+ resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.4':
+ resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.2.4':
+ resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@7.0.1':
+ resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==}
+ engines: {node: '>=22', npm: '>=6', yarn: '>=1'}
+ peerDependencies:
+ '@testing-library/dom': '>=10 <11'
+ vitest: '>= 0.32'
+ peerDependenciesMeta:
+ vitest:
+ optional: true
+
+ '@testing-library/react@16.3.2':
+ resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
+ 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
+
+ '@testing-library/user-event@14.6.4':
+ resolution: {integrity: sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/node@24.13.3':
+ resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
+
+ '@types/react-dom@19.2.4':
+ resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.18':
+ resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
+
+ '@vitejs/plugin-react@6.0.5':
+ resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ '@vitest/expect@4.1.10':
+ resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
+
+ '@vitest/mocker@4.1.10':
+ resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.10':
+ resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
+
+ '@vitest/runner@4.1.10':
+ resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
+
+ '@vitest/snapshot@4.1.10':
+ resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
+
+ '@vitest/spy@4.1.10':
+ resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
+
+ '@vitest/utils@4.1.10':
+ resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ data-urls@7.0.0:
+ resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
+ entities@8.0.0:
+ resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
+ engines: {node: '>=20.19.0'}
+
+ es-module-lexer@2.3.1:
+ resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ html-encoding-sniffer@6.0.0:
+ resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsdom@30.0.1:
+ resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==}
+ engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
+ peerDependencies:
+ canvas: ^3.2.3
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
+ jwt-decode@4.0.0:
+ resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
+ engines: {node: '>=18'}
+
+ lightningcss-android-arm64@1.33.0:
+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.33.0:
+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.33.0:
+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.33.0:
+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.33.0:
+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.33.0:
+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
+ engines: {node: '>= 12.0.0'}
+
+ lru-cache@11.5.2:
+ resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
+ engines: {node: 20 || >=22}
+
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ obug@2.1.4:
+ resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
+ engines: {node: '>=12.20.0'}
+
+ oidc-client-ts@3.5.0:
+ resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==}
+ engines: {node: '>=18'}
+
+ oxlint@1.78.0:
+ resolution: {integrity: sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ oxlint-tsgolint: '>=7.0.2001'
+ vite-plus: '*'
+ peerDependenciesMeta:
+ oxlint-tsgolint:
+ optional: true
+ vite-plus:
+ optional: true
+
+ parse5@8.0.1:
+ resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.26:
+ resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ react-dom@19.2.8:
+ resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
+ peerDependencies:
+ react: ^19.2.8
+
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
+ react-oidc-context@3.3.1:
+ resolution: {integrity: sha512-/Azvm9W4DhhOtSDBE73kFInh1b6zZRRfILKbgmk2syExMF0PCYJOn/dGdOOi2BFX8x0rCeUe45NXHU+/+xDcrQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ oidc-client-ts: ^3.1.0
+ react: '>=16.14.0'
+
+ react@19.2.8:
+ resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
+ engines: {node: '>=0.10.0'}
+
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ rolldown@1.2.4:
+ resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.2.0:
+ resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
+
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.3.0:
+ resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.1.1:
+ resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
+ engines: {node: '>=14.0.0'}
+
+ tldts-core@7.4.10:
+ resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==}
+
+ tldts@7.4.10:
+ resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==}
+ hasBin: true
+
+ tough-cookie@6.0.2:
+ resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==}
+ engines: {node: '>=16'}
+
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+ undici@8.10.0:
+ resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
+ engines: {node: '>=22.19.0'}
+
+ vite@8.2.1:
+ resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.4.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.1.10:
+ resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.1.10
+ '@vitest/browser-preview': 4.1.10
+ '@vitest/browser-webdriverio': 4.1.10
+ '@vitest/coverage-istanbul': 4.1.10
+ '@vitest/coverage-v8': 4.1.10
+ '@vitest/ui': 4.1.10
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
+ webidl-conversions@8.0.1:
+ resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
+ engines: {node: '>=20'}
+
+ whatwg-mimetype@5.0.0:
+ resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
+ engines: {node: '>=20'}
+
+ whatwg-url@16.0.1:
+ resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ whatwg-url@17.1.0:
+ resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==}
+ engines: {node: ^22.14.0 || >=24.0.0}
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
+snapshots:
+
+ '@adobe/css-tools@4.5.0': {}
+
+ '@asamuzakjp/css-color@6.0.7':
+ dependencies:
+ '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+ lru-cache: 11.5.2
+
+ '@asamuzakjp/dom-selector@8.3.2':
+ dependencies:
+ bidi-js: 1.0.3
+ css-tree: 3.2.1
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.5.2
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/runtime@7.29.7': {}
+
+ '@bramus/specificity@2.4.2':
+ dependencies:
+ css-tree: 3.2.1
+
+ '@csstools/color-helpers@6.1.0': {}
+
+ '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/color-helpers': 6.1.0
+ '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)':
+ optionalDependencies:
+ css-tree: 3.2.1
+
+ '@csstools/css-tokenizer@4.0.0': {}
+
+ '@exodus/bytes@1.15.1': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@oxc-project/types@0.144.0': {}
+
+ '@oxlint/binding-android-arm-eabi@1.78.0':
+ optional: true
+
+ '@oxlint/binding-android-arm64@1.78.0':
+ optional: true
+
+ '@oxlint/binding-darwin-arm64@1.78.0':
+ optional: true
+
+ '@oxlint/binding-darwin-x64@1.78.0':
+ optional: true
+
+ '@oxlint/binding-freebsd-x64@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-musleabihf@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-gnu@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-musl@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-ppc64-gnu@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-gnu@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-musl@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-s390x-gnu@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-gnu@1.78.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-musl@1.78.0':
+ optional: true
+
+ '@oxlint/binding-openharmony-arm64@1.78.0':
+ optional: true
+
+ '@oxlint/binding-win32-arm64-msvc@1.78.0':
+ optional: true
+
+ '@oxlint/binding-win32-ia32-msvc@1.78.0':
+ optional: true
+
+ '@oxlint/binding-win32-x64-msvc@1.78.0':
+ optional: true
+
+ '@rolldown/binding-android-arm64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.2.4':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.4':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.2.4':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/runtime': 7.29.7
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.1(@types/node@24.13.3)))':
+ dependencies:
+ '@adobe/css-tools': 4.5.0
+ '@testing-library/dom': 10.4.1
+ aria-query: 5.3.2
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ picocolors: 1.1.1
+ redent: 3.0.0
+ optionalDependencies:
+ vitest: 4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.1(@types/node@24.13.3))
+
+ '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@testing-library/dom': 10.4.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ optionalDependencies:
+ '@types/react': 19.2.18
+ '@types/react-dom': 19.2.4(@types/react@19.2.18)
+
+ '@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
+ '@types/aria-query@5.0.4': {}
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/node@24.13.3':
+ dependencies:
+ undici-types: 7.18.2
+
+ '@types/react-dom@19.2.4(@types/react@19.2.18)':
+ dependencies:
+ '@types/react': 19.2.18
+
+ '@types/react@19.2.18':
+ dependencies:
+ csstype: 3.2.3
+
+ '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@24.13.3))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 8.2.1(@types/node@24.13.3)
+
+ '@vitest/expect@4.1.10':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ chai: 6.2.2
+ tinyrainbow: 3.1.1
+
+ '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@24.13.3))':
+ dependencies:
+ '@vitest/spy': 4.1.10
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.2.1(@types/node@24.13.3)
+
+ '@vitest/pretty-format@4.1.10':
+ dependencies:
+ tinyrainbow: 3.1.1
+
+ '@vitest/runner@4.1.10':
+ dependencies:
+ '@vitest/utils': 4.1.10
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/utils': 4.1.10
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.10': {}
+
+ '@vitest/utils@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.1
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@5.2.0: {}
+
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ aria-query@5.3.2: {}
+
+ assertion-error@2.0.1: {}
+
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
+ chai@6.2.2: {}
+
+ convert-source-map@2.0.0: {}
+
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
+ css.escape@1.5.1: {}
+
+ csstype@3.2.3: {}
+
+ data-urls@7.0.0:
+ dependencies:
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ decimal.js@10.6.0: {}
+
+ dequal@2.0.3: {}
+
+ detect-libc@2.1.2: {}
+
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
+ entities@8.0.0: {}
+
+ es-module-lexer@2.3.1: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ expect-type@1.4.0: {}
+
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
+ fsevents@2.3.3:
+ optional: true
+
+ html-encoding-sniffer@6.0.0:
+ dependencies:
+ '@exodus/bytes': 1.15.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ indent-string@4.0.0: {}
+
+ is-potential-custom-element-name@1.0.1: {}
+
+ js-tokens@4.0.0: {}
+
+ jsdom@30.0.1:
+ dependencies:
+ '@asamuzakjp/css-color': 6.0.7
+ '@asamuzakjp/dom-selector': 8.3.2
+ '@bramus/specificity': 2.4.2
+ '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1)
+ '@exodus/bytes': 1.15.1
+ css-tree: 3.2.1
+ data-urls: 7.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 6.0.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.5.2
+ parse5: 8.0.1
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.2
+ undici: 8.10.0
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.1
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 17.1.0
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ jwt-decode@4.0.0: {}
+
+ lightningcss-android-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.33.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.33.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ optional: true
+
+ lightningcss@1.33.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.33.0
+ lightningcss-darwin-arm64: 1.33.0
+ lightningcss-darwin-x64: 1.33.0
+ lightningcss-freebsd-x64: 1.33.0
+ lightningcss-linux-arm-gnueabihf: 1.33.0
+ lightningcss-linux-arm64-gnu: 1.33.0
+ lightningcss-linux-arm64-musl: 1.33.0
+ lightningcss-linux-x64-gnu: 1.33.0
+ lightningcss-linux-x64-musl: 1.33.0
+ lightningcss-win32-arm64-msvc: 1.33.0
+ lightningcss-win32-x64-msvc: 1.33.0
+
+ lru-cache@11.5.2: {}
+
+ lz-string@1.5.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ mdn-data@2.27.1: {}
+
+ min-indent@1.0.1: {}
+
+ nanoid@3.3.18: {}
+
+ obug@2.1.4: {}
+
+ oidc-client-ts@3.5.0:
+ dependencies:
+ jwt-decode: 4.0.0
+
+ oxlint@1.78.0:
+ optionalDependencies:
+ '@oxlint/binding-android-arm-eabi': 1.78.0
+ '@oxlint/binding-android-arm64': 1.78.0
+ '@oxlint/binding-darwin-arm64': 1.78.0
+ '@oxlint/binding-darwin-x64': 1.78.0
+ '@oxlint/binding-freebsd-x64': 1.78.0
+ '@oxlint/binding-linux-arm-gnueabihf': 1.78.0
+ '@oxlint/binding-linux-arm-musleabihf': 1.78.0
+ '@oxlint/binding-linux-arm64-gnu': 1.78.0
+ '@oxlint/binding-linux-arm64-musl': 1.78.0
+ '@oxlint/binding-linux-ppc64-gnu': 1.78.0
+ '@oxlint/binding-linux-riscv64-gnu': 1.78.0
+ '@oxlint/binding-linux-riscv64-musl': 1.78.0
+ '@oxlint/binding-linux-s390x-gnu': 1.78.0
+ '@oxlint/binding-linux-x64-gnu': 1.78.0
+ '@oxlint/binding-linux-x64-musl': 1.78.0
+ '@oxlint/binding-openharmony-arm64': 1.78.0
+ '@oxlint/binding-win32-arm64-msvc': 1.78.0
+ '@oxlint/binding-win32-ia32-msvc': 1.78.0
+ '@oxlint/binding-win32-x64-msvc': 1.78.0
+
+ parse5@8.0.1:
+ dependencies:
+ entities: 8.0.0
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.5: {}
+
+ postcss@8.5.26:
+ dependencies:
+ nanoid: 3.3.18
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
+ punycode@2.3.1: {}
+
+ react-dom@19.2.8(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+ scheduler: 0.27.0
+
+ react-is@17.0.2: {}
+
+ react-oidc-context@3.3.1(oidc-client-ts@3.5.0)(react@19.2.8):
+ dependencies:
+ oidc-client-ts: 3.5.0
+ react: 19.2.8
+
+ react@19.2.8: {}
+
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
+ require-from-string@2.0.2: {}
+
+ rolldown@1.2.4:
+ dependencies:
+ '@oxc-project/types': 0.144.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.2.4
+ '@rolldown/binding-darwin-arm64': 1.2.4
+ '@rolldown/binding-darwin-x64': 1.2.4
+ '@rolldown/binding-freebsd-x64': 1.2.4
+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.4
+ '@rolldown/binding-linux-arm64-gnu': 1.2.4
+ '@rolldown/binding-linux-arm64-musl': 1.2.4
+ '@rolldown/binding-linux-ppc64-gnu': 1.2.4
+ '@rolldown/binding-linux-s390x-gnu': 1.2.4
+ '@rolldown/binding-linux-x64-gnu': 1.2.4
+ '@rolldown/binding-linux-x64-musl': 1.2.4
+ '@rolldown/binding-openharmony-arm64': 1.2.4
+ '@rolldown/binding-win32-arm64-msvc': 1.2.4
+ '@rolldown/binding-win32-x64-msvc': 1.2.4
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
+ scheduler@0.27.0: {}
+
+ siginfo@2.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ stackback@0.0.2: {}
+
+ std-env@4.2.0: {}
+
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
+
+ symbol-tree@3.2.4: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.3.0: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
+ tinyrainbow@3.1.1: {}
+
+ tldts-core@7.4.10: {}
+
+ tldts@7.4.10:
+ dependencies:
+ tldts-core: 7.4.10
+
+ tough-cookie@6.0.2:
+ dependencies:
+ tldts: 7.4.10
+
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
+
+ typescript@6.0.3: {}
+
+ undici-types@7.18.2: {}
+
+ undici@8.10.0: {}
+
+ vite@8.2.1(@types/node@24.13.3):
+ dependencies:
+ lightningcss: 1.33.0
+ picomatch: 4.0.5
+ postcss: 8.5.26
+ rolldown: 1.2.4
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.13.3
+ fsevents: 2.3.3
+
+ vitest@4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.1(@types/node@24.13.3)):
+ dependencies:
+ '@vitest/expect': 4.1.10
+ '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3))
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/runner': 4.1.10
+ '@vitest/snapshot': 4.1.10
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ es-module-lexer: 2.3.1
+ expect-type: 1.4.0
+ magic-string: 0.30.21
+ obug: 2.1.4
+ pathe: 2.0.3
+ picomatch: 4.0.5
+ std-env: 4.2.0
+ tinybench: 2.9.0
+ tinyexec: 1.3.0
+ tinyglobby: 0.2.17
+ tinyrainbow: 3.1.1
+ vite: 8.2.1(@types/node@24.13.3)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 24.13.3
+ jsdom: 30.0.1
+ transitivePeerDependencies:
+ - msw
+
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
+ webidl-conversions@8.0.1: {}
+
+ whatwg-mimetype@5.0.0: {}
+
+ whatwg-url@16.0.1:
+ dependencies:
+ '@exodus/bytes': 1.15.1
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ whatwg-url@17.1.0:
+ dependencies:
+ '@exodus/bytes': 1.15.1
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 000000000..6893eb132
--- /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 000000000..e9522193d
--- /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 000000000..d42f4a4d3
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,104 @@
+#root {
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 1.5rem;
+ text-align: left;
+}
+
+.centered {
+ text-align: center;
+ padding-top: 4rem;
+}
+
+.app-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.app-header > div {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.error {
+ color: #b91c1c;
+}
+
+.post-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.post-list-item {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ margin-bottom: 0.5rem;
+ border: 1px solid #3333;
+ border-radius: 8px;
+ background: none;
+ cursor: pointer;
+ text-align: left;
+ font-size: 1rem;
+}
+
+.post-badge {
+ font-size: 0.75rem;
+ opacity: 0.7;
+ text-transform: uppercase;
+}
+
+.popup-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.popup-panel {
+ position: relative;
+ background: canvas;
+ color: canvastext;
+ max-width: 560px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+ padding: 2rem;
+ border-radius: 12px;
+}
+
+.popup-close {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+}
+
+.post-meta {
+ opacity: 0.7;
+ font-size: 0.85rem;
+}
+
+.post-body {
+ white-space: pre-wrap;
+}
+
+.popup-placeholder {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ border: 1px dashed #3336;
+ border-radius: 8px;
+ font-size: 0.85rem;
+ opacity: 0.7;
+}
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
new file mode 100644
index 000000000..fb77171fa
--- /dev/null
+++ b/frontend/src/App.test.tsx
@@ -0,0 +1,102 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import App from "./App";
+
+const signinRedirect = vi.fn();
+const signoutRedirect = vi.fn();
+let mockAuth: Record;
+
+vi.mock("react-oidc-context", () => ({
+ useAuth: () => mockAuth,
+}));
+
+beforeEach(() => {
+ signinRedirect.mockReset();
+ signoutRedirect.mockReset();
+ mockAuth = {
+ isLoading: false,
+ isAuthenticated: false,
+ error: undefined,
+ user: undefined,
+ signinRedirect,
+ signoutRedirect,
+ };
+});
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+describe("App, unauthenticated", () => {
+ it("shows a login button that starts the real OIDC redirect", async () => {
+ render( );
+ const button = screen.getByRole("button", { name: /log in/i });
+ await userEvent.click(button);
+ expect(signinRedirect).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe("App, authenticated", () => {
+ beforeEach(() => {
+ mockAuth = {
+ ...mockAuth,
+ isAuthenticated: true,
+ user: {
+ access_token: "test-access-token",
+ profile: { preferred_username: "demo.analyst" },
+ },
+ };
+ });
+
+ it("fetches and renders the post list, then opens a detail popup on click", async () => {
+ const fetchMock = vi.fn((input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url.endsWith("/api/posts")) {
+ return Promise.resolve(
+ new Response(
+ JSON.stringify([
+ {
+ post_id: "post-1",
+ post_title: "Public post",
+ voc_type_code: "voc",
+ visibility_code: "public",
+ created_at: "2026-01-01T00:00:00Z",
+ },
+ ]),
+ { status: 200 },
+ ),
+ );
+ }
+ if (url.endsWith("/api/posts/post-1")) {
+ return Promise.resolve(
+ new Response(
+ JSON.stringify({
+ post_id: "post-1",
+ post_title: "Public post",
+ post_body: "The full body text.",
+ voc_type_code: "voc",
+ visibility_code: "public",
+ created_at: "2026-01-01T00:00:00Z",
+ }),
+ { status: 200 },
+ ),
+ );
+ }
+ return Promise.reject(new Error(`unexpected fetch: ${url}`));
+ });
+ vi.stubGlobal("fetch", fetchMock);
+
+ render( );
+
+ await screen.findByText("Public post");
+ expect(fetchMock).toHaveBeenCalledWith(
+ expect.stringContaining("/api/posts"),
+ expect.objectContaining({ headers: { Authorization: "Bearer test-access-token" } }),
+ );
+
+ await userEvent.click(screen.getByText("Public post"));
+
+ await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
+ });
+});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 000000000..1c0b4b510
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,127 @@
+import { useEffect, useState } from "react";
+import { useAuth } from "react-oidc-context";
+import { fetchPost, fetchPosts, type PostDetail, type PostSummary } from "./api";
+import "./App.css";
+
+function PostDetailPopup({
+ postId,
+ accessToken,
+ onClose,
+}: {
+ postId: string;
+ accessToken: string;
+ onClose: () => void;
+}) {
+ const [post, setPost] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ setPost(null);
+ setError(null);
+ fetchPost(accessToken, postId).then(setPost).catch((err) => setError(String(err)));
+ }, [postId, accessToken]);
+
+ return (
+
+
event.stopPropagation()}>
+
+ ×
+
+ {error &&
{error}
}
+ {!post && !error &&
Loading...
}
+ {post && (
+ <>
+
{post.post_title}
+
+ {post.voc_type_code} · {post.visibility_code} ·{" "}
+ {new Date(post.created_at).toLocaleString()}
+
+
{post.post_body}
+ {/* Event Lineage / Keyman / Knowledge Graph / LLM chat panels
+ land in Phase 2-4 -- this popup shell is the attachment
+ point (Figma frame SBpgot7uTvMxEaxUwvoc0S). */}
+
+ Event Lineage, Keyman, and Knowledge Graph views arrive in a
+ later phase.
+
+ >
+ )}
+
+
+ );
+}
+
+function PostList({ accessToken }: { accessToken: string }) {
+ const [posts, setPosts] = useState(null);
+ const [error, setError] = useState(null);
+ const [selectedPostId, setSelectedPostId] = useState(null);
+
+ useEffect(() => {
+ fetchPosts(accessToken).then(setPosts).catch((err) => setError(String(err)));
+ }, [accessToken]);
+
+ if (error) return {error}
;
+ if (!posts) return Loading posts...
;
+ if (posts.length === 0) return No posts visible to this account yet -- try `make seed`.
;
+
+ return (
+ <>
+
+ {posts.map((post) => (
+
+ setSelectedPostId(post.post_id)}>
+ {post.post_title}
+ {post.visibility_code}
+
+
+ ))}
+
+ {selectedPostId && (
+ setSelectedPostId(null)}
+ />
+ )}
+ >
+ );
+}
+
+export default function App() {
+ const auth = useAuth();
+
+ if (auth.isLoading) {
+ return Loading authentication state...
;
+ }
+
+ if (auth.error) {
+ return Authentication error: {auth.error.message}
;
+ }
+
+ if (!auth.isAuthenticated) {
+ return (
+
+ LineageWeave
+ auth.signinRedirect()}>Log in
+
+ );
+ }
+
+ const accessToken = auth.user?.access_token;
+ if (!accessToken) {
+ return Authenticated, but no access token was returned.
;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
new file mode 100644
index 000000000..15084c175
--- /dev/null
+++ b/frontend/src/api.ts
@@ -0,0 +1,31 @@
+import { config } from "./config";
+
+export interface PostSummary {
+ post_id: string;
+ post_title: string;
+ voc_type_code: string;
+ visibility_code: string;
+ created_at: string;
+}
+
+export interface PostDetail extends PostSummary {
+ post_body: string;
+}
+
+async function backendFetch(path: string, accessToken: string): Promise {
+ const response = await fetch(`${config.backendBaseUrl}${path}`, {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ });
+ if (!response.ok) {
+ throw new Error(`${path} -> HTTP ${response.status}`);
+ }
+ return response.json() as Promise;
+}
+
+export function fetchPosts(accessToken: string): Promise {
+ return backendFetch("/api/posts", accessToken);
+}
+
+export function fetchPost(accessToken: string, postId: string): Promise {
+ return backendFetch(`/api/posts/${postId}`, accessToken);
+}
diff --git a/frontend/src/config.ts b/frontend/src/config.ts
new file mode 100644
index 000000000..2d7b7342b
--- /dev/null
+++ b/frontend/src/config.ts
@@ -0,0 +1,8 @@
+/** Env-driven config -- no hardcoded backend/Keycloak URLs. Vite only
+ * exposes `VITE_`-prefixed variables to client code by design. */
+
+export const config = {
+ keycloakIssuer: import.meta.env.VITE_KEYCLOAK_ISSUER ?? "http://localhost:18080/realms/lineageweave-demo",
+ keycloakClientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID ?? "lineageweave-frontend",
+ backendBaseUrl: import.meta.env.VITE_BACKEND_BASE_URL ?? "http://localhost:18420",
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 000000000..5fb331302
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,111 @@
+:root {
+ --text: #6b6375;
+ --text-h: #08060d;
+ --bg: #fff;
+ --border: #e5e4e7;
+ --code-bg: #f4f3ec;
+ --accent: #aa3bff;
+ --accent-bg: rgba(170, 59, 255, 0.1);
+ --accent-border: rgba(170, 59, 255, 0.5);
+ --social-bg: rgba(244, 243, 236, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ font: 18px/145% var(--sans);
+ letter-spacing: 0.18px;
+ color-scheme: light dark;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --text: #9ca3af;
+ --text-h: #f3f4f6;
+ --bg: #16171d;
+ --border: #2e303a;
+ --code-bg: #1f2028;
+ --accent: #c084fc;
+ --accent-bg: rgba(192, 132, 252, 0.15);
+ --accent-border: rgba(192, 132, 252, 0.5);
+ --social-bg: rgba(47, 48, 58, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+ }
+
+ #social .button-icon {
+ filter: invert(1) brightness(2);
+ }
+}
+
+#root {
+ width: 1126px;
+ max-width: 100%;
+ margin: 0 auto;
+ text-align: center;
+ border-inline: 1px solid var(--border);
+ min-height: 100svh;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+}
+
+h1,
+h2 {
+ font-family: var(--heading);
+ font-weight: 500;
+ color: var(--text-h);
+}
+
+h1 {
+ font-size: 56px;
+ letter-spacing: -1.68px;
+ margin: 32px 0;
+ @media (max-width: 1024px) {
+ font-size: 36px;
+ margin: 20px 0;
+ }
+}
+h2 {
+ font-size: 24px;
+ line-height: 118%;
+ letter-spacing: -0.24px;
+ margin: 0 0 8px;
+ @media (max-width: 1024px) {
+ font-size: 20px;
+ }
+}
+p {
+ margin: 0;
+}
+
+code,
+.counter {
+ font-family: var(--mono);
+ display: inline-flex;
+ border-radius: 4px;
+ color: var(--text-h);
+}
+
+code {
+ font-size: 15px;
+ line-height: 135%;
+ padding: 4px 8px;
+ background: var(--code-bg);
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 000000000..ebd64d20d
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,25 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { AuthProvider } from "react-oidc-context";
+import "./index.css";
+import App from "./App.tsx";
+import { config } from "./config";
+
+const oidcConfig = {
+ authority: config.keycloakIssuer,
+ client_id: config.keycloakClientId,
+ redirect_uri: window.location.origin,
+ post_logout_redirect_uri: window.location.origin,
+ onSigninCallback: () => {
+ // Strip the OIDC response params (code/state) from the URL after login.
+ window.history.replaceState({}, document.title, window.location.pathname);
+ },
+};
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+ ,
+);
diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts
new file mode 100644
index 000000000..f149f27ae
--- /dev/null
+++ b/frontend/src/setupTests.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom/vitest";
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 000000000..8895906bf
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1,11 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_KEYCLOAK_ISSUER?: string;
+ readonly VITE_KEYCLOAK_CLIENT_ID?: string;
+ readonly VITE_BACKEND_BASE_URL?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 000000000..6830b6f75
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 000000000..1ffef600d
--- /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 000000000..8455dcbc2
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 000000000..4ab53f1a3
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,12 @@
+///
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./src/setupTests.ts'],
+ },
+})
diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py
index 74a63cbc7..8584783a0 100644
--- a/lineageweave/__init__.py
+++ b/lineageweave/__init__.py
@@ -12,4 +12,4 @@
__all__ = ["Edge", "Record", "Tree", "reconstruct"]
-__version__ = "0.6.0"
+__version__ = "0.7.0"
diff --git a/pyproject.toml b/pyproject.toml
index ffb44011c..6d1bb4853 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "0.6.0"
+version = "0.7.0"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
diff --git a/tests/test_http_client.py b/tests/test_http_client.py
index fd2a5a717..3dce75097 100644
--- a/tests/test_http_client.py
+++ b/tests/test_http_client.py
@@ -168,6 +168,7 @@ def test_dockerfiles_pin_digest_and_declare_non_root_user() -> None:
"docker/postgres-init/Dockerfile",
"docker/keycloak/Dockerfile",
"backend/Dockerfile",
+ "frontend/Dockerfile",
):
text = (root / relative).read_text()
assert "@sha256:" in text, f"{relative} must pin the base image by digest"
diff --git a/uv.lock b/uv.lock
index 673582e4b..c107ad33e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -438,7 +438,7 @@ wheels = [
[[package]]
name = "lineageweave"
-version = "0.6.0"
+version = "0.7.0"
source = { virtual = "." }
dependencies = [
{ name = "certifi" },