diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..0944703 --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,89 @@ +# spa β€” Roadmap + +A React + TypeScript single-page application for end-user operators of the TRM platform: race directors, marshals, timekeepers, and (post-Phase 4 of [[directus]]) public-facing participants. Talks to [[directus]] for REST/GraphQL + business-plane WebSocket and to [[processor]] for the live-position WebSocket firehose. + +This file is the single navigation hub for all SPA implementation planning. Each phase has its own folder with a README and granular task files. Update statuses here as work lands. + +## Status legend + +| Symbol | Meaning | +| ------ | ----------------------------- | +| ⬜ | Not started | +| 🟦 | Planned (designed, not coded) | +| 🟨 | In progress | +| 🟩 | Done | +| ⏸ | Paused / blocked | +| ❄️ | Frozen / future / optional | + +## Architectural anchors + +The SPA is specified by the wiki at `../docs/wiki/`. Implementing agents should read these pages before starting any task: + +- **This service** β€” `docs/wiki/entities/react-spa.md` +- **Map subsystem** β€” `docs/wiki/concepts/maps-architecture.md`, `docs/wiki/sources/traccar-maps-architecture.md` +- **Live channel** β€” `docs/wiki/concepts/live-channel-architecture.md`, `docs/wiki/synthesis/processor-ws-contract.md` +- **Auth + business plane** β€” `docs/wiki/entities/directus.md`, `docs/wiki/synthesis/directus-schema-draft.md` +- **System architecture** β€” `docs/wiki/sources/gps-tracking-architecture.md`, `docs/wiki/concepts/plane-separation.md` + +## Non-negotiable design rules + +These rules govern every task. Any deviation must be discussed and documented as a decision before code lands. + +1. **Singleton MapLibre.** A single `maplibregl.Map` instance lives in a module-level variable, attached to a detached `
`. React mounts/unmounts the div via refs across page navigation. WebGL context never recreates. Pattern is documented in `docs/wiki/concepts/maps-architecture.md`. +2. **Side-effect-only `Map*` components.** Every map-participating component returns `null` and uses `useEffect` for setup + cleanup, with a separate effect for `setData` updates. No DOM marker components, no `react-map-gl`. +3. **rAF coalescer at the WS boundary.** Position messages buffer per-device; one `requestAnimationFrame` tick flushes the latest snapshot to the Zustand store. Per-message dispatch is the failure mode `traccar-web` exhibits β€” we don't replicate it. +4. **Same-origin everything.** SPA bundle, [[directus]] REST/WS, and [[processor]] WS all served under one origin via reverse proxy. Vite dev proxy in dev; Traefik in stage/prod. Cross-origin breaks the cookie auth flow. +5. **In-memory access token, httpOnly refresh cookie.** Access token never touches `localStorage`. Refresh cookie is `httpOnly + Secure + SameSite=Lax`. Refresh on 401 via Directus's `/auth/refresh`. +6. **Role-aware UI shape from day one.** Even though everyone's admin until [[directus]] Phase 4, the route guards and conditional rendering check `user.role` rather than hard-coding "everyone sees everything." Retrofitting is painful; baking it in costs nothing. +7. **Runtime config, not build-time.** Per-environment values (Directus URL, processor WS URL, optional Google Maps key) come from `/config.json` served by the proxy. One image, multiple environments. No rebuild to switch envs. +8. **Native PostGIS GeoJSON.** Geometry round-trips through `ST_AsGeoJSON(geometry)` on the server, not WKT. The SPA never imports `wellknown` or runs WKT parsing in the client. + +## Phases + +### Phase 1 β€” Foundation + +**Status:** 🟨 In progress (1.1 done) +**Outcome:** A deployable empty-shell SPA: scaffold + stack rounded out + Directus cookie auth flow + protected routes + Gitea CI + compose deploy block. End state: an operator can browse to `https://stage.trmtracking.org`, log in with a Directus credential, see a placeholder home page, log out. No live map yet β€” that's Phase 2. + +[**See `phase-1-foundation/README.md`**](./phase-1-foundation/README.md) + +| # | Task | Status | Landed in | +| ---- | ------------------------------------------------------------------------------------------------------------ | ------ | --------- | +| 1.1 | Project scaffold (Vite + React + TS) | 🟩 | (manual) | +| 1.2 | [Stack rounding-out (Tailwind + shadcn/ui + deps + Prettier)](./phase-1-foundation/02-stack-rounding-out.md) | 🟩 | local (uncommitted) | +| 1.3 | [Vite dev proxy + path aliases + tsconfig hardening](./phase-1-foundation/03-vite-dev-proxy.md) | ⬜ | β€” | +| 1.4 | [Runtime config endpoint](./phase-1-foundation/04-runtime-config.md) | ⬜ | β€” | +| 1.5 | [Directus auth client (cookie mode + refresh)](./phase-1-foundation/05-directus-auth-client.md) | ⬜ | β€” | +| 1.6 | [Login page](./phase-1-foundation/06-login-page.md) | ⬜ | β€” | +| 1.7 | [Routing skeleton (TanStack Router + role-aware guards)](./phase-1-foundation/07-routing-skeleton.md) | ⬜ | β€” | +| 1.8 | [Logout flow](./phase-1-foundation/08-logout-flow.md) | ⬜ | β€” | +| 1.9 | [Gitea CI + Dockerfile + nginx static serve](./phase-1-foundation/09-gitea-ci-and-dockerfile.md) | ⬜ | β€” | +| 1.10 | [Compose service block in trm/deploy](./phase-1-foundation/10-deploy-compose-block.md) | ⬜ | β€” | + +### Phase 2 β€” Live monitoring map + +**Status:** ⬜ Not started β€” depends on [[processor]] Phase 1.5 landing +**Outcome:** The dogfood-day deliverable. MapLibre singleton + tile-source switcher + sprite preload + WS client with rAF coalescer + Zustand position store + `MapPositions` (clustered + selected) + `MapTrails` (bounded ring buffer) + event picker + camera control trio + connection-status indicator. Operators on race day open one page, pick the active event, and watch their field move in real time. + +[**See `phase-2-live-map/README.md`**](./phase-2-live-map/README.md) + +### Phase 3 β€” Dogfood readiness + +**Status:** ⬜ Not started +**Outcome:** Operational polish a closed pilot needs but a demo doesn't: error boundaries that don't blank-screen the map, connection-state UI that tells the operator "WS reconnecting", mobile-responsive baseline (doesn't crash on a phone in the field), per-device detail panel, operator-friendly empty/loading states. + +[**See `phase-3-dogfood-readiness/README.md`**](./phase-3-dogfood-readiness/README.md) + +### Phase 4 β€” Future / optional + +**Status:** ❄️ Not committed +[**See `phase-4-future/README.md`**](./phase-4-future/README.md) + +Ideas on radar: geometry editor (depends on [[directus]] Phase 2 collections), replay mode, heatmaps / deck.gl, i18n (Albanian), dark mode, E2E tests. + +## Operating model + +- **Implementation agent contract.** Each task file is self-sufficient: goal, deliverables, specification, acceptance criteria. An agent should be able to complete one task without reading the whole wiki β€” but should skim the wiki references at the top of the task before starting. +- **Sequence within a phase.** Task numbering reflects intended order. Soft dependencies are explicit in each task's "Depends on" field. Tasks with no dependencies on each other can be done in parallel. +- **Status updates.** When a task is started, change its row in this ROADMAP to 🟨 and the task file's status badge accordingly. When done, 🟩 + a one-line note in the task file's "Done" section pointing at the merging commit/PR. +- **Drift control.** If implementation diverges from a task's spec, update the task file _before_ the diverging code lands, with a note explaining why. Do not let plans rot. diff --git a/.planning/phase-1-foundation/02-stack-rounding-out.md b/.planning/phase-1-foundation/02-stack-rounding-out.md new file mode 100644 index 0000000..4177021 --- /dev/null +++ b/.planning/phase-1-foundation/02-stack-rounding-out.md @@ -0,0 +1,110 @@ +# Task 1.2 β€” Stack rounding-out (Tailwind + shadcn/ui + deps + Prettier) + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.1 (scaffold) +**Wiki refs:** `docs/wiki/entities/react-spa.md` Β§Stack + +## Goal + +Take the bare Vite + React + TS scaffold from 1.1 and round it out into a real app stack: Tailwind CSS, shadcn/ui primitives, TanStack Router + Query, Zustand, `@directus/sdk`, zod, react-hook-form, Prettier. After this task, every dependency the rest of Phase 1 needs is installed and configured; subsequent tasks build features on top. + +This is mechanical setup, not feature work. Don't introduce features here β€” keep the placeholder `App.tsx` returning "SPA" until 1.7 wires the router. + +## Deliverables + +- **Tailwind CSS 4 via the Vite plugin:** + - `pnpm add -D tailwindcss @tailwindcss/vite` + - `vite.config.ts` updated to include the Tailwind plugin (additive; don't disturb the React plugin). + - `src/styles/globals.css` with `@import "tailwindcss";` (Tailwind 4 syntax β€” single import, no `@tailwind base/components/utilities` triplet). + - `src/main.tsx` imports `./styles/globals.css`. + - Sanity check: `
test
` in `App.tsx` renders red. +- **shadcn/ui initialised:** + - `npx shadcn@latest init` β€” accept the defaults that fit our setup (Tailwind 4, TS, `src/ui/primitives` as the `components` path, `src/lib/utils.ts` for `cn`). + - `components.json` committed. + - Add the bare-minimum primitives we'll need in 1.6 (login page) and 1.7 (routing): `button`, `input`, `label`, `form`, `card`, `alert`. Run `npx shadcn@latest add button input label form card alert` β€” these populate `src/ui/primitives/` as plain TS files we own and edit. +- **TanStack Router + Query:** + - `pnpm add @tanstack/react-router @tanstack/react-query` + - `pnpm add -D @tanstack/router-plugin @tanstack/router-devtools @tanstack/react-query-devtools` + - Don't wire them yet β€” that's 1.7. Just install. +- **State + data libs:** + - `pnpm add zustand @directus/sdk zod react-hook-form @hookform/resolvers` +- **Prettier + ESLint integration:** + - `pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier` + - `.prettierrc` with sensible defaults (single quotes, semi: true, printWidth: 100, trailingComma: 'all'). + - `.prettierignore` matching `.gitignore` plus `pnpm-lock.yaml`, `dist/`, `public/`. + - `eslint.config.js` extended to include `eslint-config-prettier` last (turns off conflicting rules) and the prettier plugin's recommended config. + - `package.json` `scripts`: + - `"format": "prettier --write ."` + - `"format:check": "prettier --check ."` + - `"typecheck": "tsc -b --noEmit"` (replaces the implicit typecheck inside the existing `build` script for a faster CI gate). +- `README.md` updated with: stack list, common scripts, "how to add a shadcn component." + +## Specification + +### Why Tailwind 4 (the Vite-plugin path) + +Tailwind 4 ships a Vite plugin that does the JIT generation in-process β€” no PostCSS dance, no `@tailwind` directives, single `@import "tailwindcss";` in the entry CSS. Faster, fewer config files, the modern path. The shadcn/ui CLI auto-detects Tailwind 4 and configures correctly. + +### Why a separate `src/lib/` for `cn` and `query-client.ts` + +shadcn/ui expects `src/lib/utils.ts` to exist (it imports `cn` from there in every primitive). The `query-client.ts` (in 1.5) lives alongside it β€” both are framework-glue, not feature code. + +### What we don't install yet + +- **MapLibre GL JS** β€” Phase 2. +- **`@mapbox/mapbox-gl-draw`** β€” deferred until geometry editing in Phase 4. +- **`maplibre-google-maps`** β€” Phase 2, optional / runtime-config-gated. +- **Vitest + React Testing Library** β€” Phase 3. +- **i18n libraries** β€” deferred (English-only for dogfood). + +### Prettier ↔ ESLint coexistence + +ESLint 10 with the `typescript-eslint` flat config has different opinions from Prettier on quote style and trailing commas. The standard fix is: `eslint-config-prettier` last in the chain (disables the conflicting style rules), `eslint-plugin-prettier` in `recommended` mode (so ESLint reports Prettier diffs as ESLint errors, single source of truth in CI). + +Don't try to make ESLint also do formatting β€” Prettier owns formatting, ESLint owns lint. The two-tool split is the boring-correct setup. + +### `package.json` final shape (illustrative) + +```json +{ + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint .", + "typecheck": "tsc -b --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check ." + } +} +``` + +CI (task 1.9) will run `typecheck`, `lint`, `format:check`, `build` in that order. + +## Acceptance criteria + +- [ ] `pnpm install` completes without warnings about peer-dep conflicts. +- [ ] `pnpm dev` starts and the page shows "SPA" still rendering, plus a Tailwind-styled colour test (e.g. red background) to prove Tailwind works. +- [ ] `pnpm typecheck` clean. +- [ ] `pnpm lint` clean. +- [ ] `pnpm format:check` clean. +- [ ] `pnpm build` produces `dist/` with no warnings. +- [ ] `npx shadcn@latest add card` (or any primitive) works β€” proves shadcn/ui is configured correctly. +- [ ] `package.json` `dependencies` section includes (at minimum): `@directus/sdk`, `@hookform/resolvers`, `@tanstack/react-query`, `@tanstack/react-router`, `react`, `react-dom`, `react-hook-form`, `zod`, `zustand`. `devDependencies` includes the rest. + +## Risks / open questions + +- **Tailwind 4 + shadcn/ui compatibility.** shadcn supports Tailwind 4 as of late 2024. If the CLI throws an error during init, fall back to Tailwind 3 + the PostCSS plugin (older but stable). Document the choice in the task's Done section. +- **TanStack Router type inference.** The router type generation is done by the Vite plugin; without it, route-tree types don't propagate. 1.3 pulls in the Vite plugin alongside the proxy config; for now leave the router uninstalled-as-feature. +- **React 19 + libs.** Most ecosystem libs support React 19 by now (early 2026). If any of these has a peer-dep complaint, document and decide between forcing the install or downgrading the lib in question. + +## Done + +Stack rounded out: Tailwind 4 via `@tailwindcss/vite`, shadcn/ui (slate base / new-york style) with primitives `button`, `input`, `label`, `form`, `card`, `alert` in `src/ui/primitives/`, TanStack Router 1.169 + Query 5.100 (devtools + router-plugin in devDeps, not yet wired β€” that's 1.7), Zustand 5, `@directus/sdk` 21, zod 4, react-hook-form 7 + `@hookform/resolvers`, `class-variance-authority` + `clsx` + `tailwind-merge` + `lucide-react` (shadcn runtime), Prettier 3 + `eslint-config-prettier` + `eslint-plugin-prettier` integrated last in `eslint.config.js`. Path alias `@/*` β†’ `./src/*` set up in both `tsconfig.json` and `tsconfig.app.json` (TS 6 deprecates `baseUrl`; paths now resolve relative to the config file). Scripts: `dev`, `build`, `preview`, `lint`, `typecheck`, `format`, `format:check`, `test` (placeholder). README rewritten with stack/scripts/shadcn-add instructions. + +**Deviation from spec:** the path alias was originally scoped to task 1.3, but shadcn's `add` CLI needs the alias resolvable to populate primitives at the right path β€” without it shadcn created a literal `@\` directory. Set up minimally here; 1.3 still owns the rest of the tsconfig hardening and the dev proxy. + +**Smoke check:** `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm build` all green. `App.tsx` renders a Tailwind-styled centered card with a shadcn `Button` to prove the toolchain works end-to-end. Bundle: 222KB raw / 70KB gzipped. + +(Commit SHA pending β€” work landed locally, not yet committed.) diff --git a/.planning/phase-1-foundation/03-vite-dev-proxy.md b/.planning/phase-1-foundation/03-vite-dev-proxy.md new file mode 100644 index 0000000..4aec658 --- /dev/null +++ b/.planning/phase-1-foundation/03-vite-dev-proxy.md @@ -0,0 +1,135 @@ +# Task 1.3 β€” Vite dev proxy + path aliases + tsconfig hardening + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.2 +**Wiki refs:** `docs/wiki/entities/react-spa.md` Β§Auth pattern + +## Goal + +Configure Vite's dev server to proxy `/api`, `/ws-business`, `/ws-live`, and `/config.json` to the appropriate local services so the SPA dev experience matches the stage/prod reverse-proxy topology. Same-origin in dev means cookies flow correctly, identical to stage/prod where Traefik does the routing. Also: add path aliases (`@/`) so imports stay short, and tighten `tsconfig.json` for stricter type safety. + +After this task, `pnpm dev` against a locally-running Directus + (eventually) Processor lets the SPA exercise the real auth flow without CORS workarounds. + +## Deliverables + +- `vite.config.ts` updated: + - `server.proxy` configured for: + - `/api` β†’ `http://localhost:8055` (Directus REST/GraphQL). + - `/ws-business` β†’ `ws://localhost:8055/websocket` (Directus business-plane WS), with `ws: true` and `rewrite` to strip `/ws-business`. + - `/ws-live` β†’ `ws://localhost:8081` (Processor live WS, once Phase 1.5 lands), with `ws: true` and `rewrite` to strip `/ws-live`. + - Don't proxy `/config.json` β€” it's served from `public/` by Vite directly in dev. + - `resolve.alias` for `@/` β†’ `src/`. + - `server.port` pinned to `5173` (Vite default; mention in README so anyone running other Vite apps avoids the conflict). +- `tsconfig.app.json` updated: + - `compilerOptions.baseUrl` to `.`, `paths` to `{ "@/*": ["src/*"] }`. + - Add `noUncheckedIndexedAccess: true` (matches `tcp-ingestion` / `processor` strictness). + - Add `noImplicitOverride: true`. + - `verbatimModuleSyntax: true` is fine if already on; otherwise leave for now. +- `README.md` updated: a "local dev" section listing the assumed local services + how to override the proxy targets via env vars (next bullet). +- Optional: env-var override for proxy targets: + - `VITE_DEV_DIRECTUS_URL` (default `http://localhost:8055`). + - `VITE_DEV_PROCESSOR_WS_URL` (default `ws://localhost:8081`). + - Read in `vite.config.ts` via `loadEnv`. + - Document in `.env.dev.example` (committed) and `.env.dev.local` (gitignored). + +## Specification + +### Why proxy in dev instead of CORS + +CORS-with-credentials requires: + +1. The browser sending `withCredentials: true` on every request. +2. The server responding with `Access-Control-Allow-Origin: ` (not `*`) and `Access-Control-Allow-Credentials: true`. +3. Cookies being scoped correctly (`SameSite=None; Secure` for cross-site β€” which means HTTPS even in dev). + +This works but it's three places that must agree, and `SameSite=None; Secure` requires localhost-with-TLS in dev (extra setup). The dev proxy collapses all of it into "everything is one origin," matching stage/prod's reverse-proxy topology. **Pick the dev proxy.** + +### Proxy config shape + +```ts +// vite.config.ts +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import path from 'node:path'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), 'VITE_'); + const directusUrl = env.VITE_DEV_DIRECTUS_URL ?? 'http://localhost:8055'; + const processorWsUrl = env.VITE_DEV_PROCESSOR_WS_URL ?? 'ws://localhost:8081'; + + return { + plugins: [react(), tailwindcss()], + resolve: { + alias: { '@': path.resolve(__dirname, './src') }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: directusUrl, + changeOrigin: true, + rewrite: (p) => p.replace(/^\/api/, ''), + }, + '/ws-business': { + target: directusUrl, + ws: true, + changeOrigin: true, + rewrite: (p) => p.replace(/^\/ws-business/, '/websocket'), + }, + '/ws-live': { + target: processorWsUrl, + ws: true, + changeOrigin: true, + rewrite: (p) => p.replace(/^\/ws-live/, ''), + }, + }, + }, + }; +}); +``` + +The path namespacing (`/api`, `/ws-business`, `/ws-live`) is what the SPA code calls. The proxy strips the namespace and forwards. Stage/prod's Traefik does the same routing under the public domain β€” same paths, same code, no env branching in the SPA. + +### Path alias + +`@/` is the convention shadcn/ui assumes (its `components.json` config wires `@/components/...`, `@/lib/utils`). Aligning Vite + tsconfig with it means imports look like: + +```ts +import { Button } from '@/ui/primitives/button'; +import { useAuthStore } from '@/auth/store'; +``` + +instead of `../../../ui/primitives/button`. Don't add other top-level aliases β€” `@/` for `src/` is enough. + +### Why `noUncheckedIndexedAccess` + +Without it, `arr[0]` is typed as `T` even when the array might be empty. With it, `arr[0]` is typed `T | undefined`, forcing a check. Catches a real class of bugs in map-position code (where "first device" assumptions are easy to make and easy to get wrong when the array is empty during reconnect). + +The processor and tcp-ingestion both have this on. Match. + +### Vite proxy and WebSocket behaviour + +The `ws: true` flag on the proxy entry tells Vite (which uses `http-proxy-middleware`) to forward the upgrade request. Cookies attached to the upgrade carry through transparently β€” that's the whole point of running same-origin in dev. + +One gotcha: the proxy's `target` for a WS route must use a `ws://` or `wss://` scheme, _not_ `http://`. If you accidentally use `http://`, the upgrade fails silently and the SPA gets a connection error. Document in the README. + +## Acceptance criteria + +- [ ] `pnpm dev` starts at `http://localhost:5173`; the SPA loads. +- [ ] With Directus running locally on `:8055`, `fetch('/api/server/info')` from the browser console returns Directus's server info β€” proxy is forwarding. +- [ ] WebSocket smoke test: `new WebSocket('ws://localhost:5173/ws-business')` connects (returns code 1006 if Directus's WS endpoint is locked down without auth, but the upgrade itself succeeds β€” that's the test). +- [ ] `import { foo } from '@/auth/foo'` resolves both at type-check time and at runtime. +- [ ] `pnpm typecheck` is stricter β€” adding `arr[0].whatever` without a check should now fail. +- [ ] No CORS errors in the browser console when calling `/api/...` from `http://localhost:5173`. + +## Risks / open questions + +- **Directus's WS path is `/websocket`, not `/ws`.** Confirmed by reading Directus 11.x source. The proxy `rewrite` rule reflects this. +- **Processor WS path is unsettled.** [[processor-ws-contract]] uses `/processor/ws` as illustrative; the actual path will be set by the proxy config in stage/prod (`trm/deploy`). For dev we assume the Processor binds to `:8081` with no path prefix, and the proxy adds the `/ws-live` prefix on the SPA side. Reconcile if Phase 1.5 lands with a different path convention. +- **Multiple Vite apps on one machine.** Pinning to `:5173` collides if another Vite app is running. Document the override (`VITE_PORT=5174 pnpm dev` works as a one-off; permanent change needs the config edit). + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/04-runtime-config.md b/.planning/phase-1-foundation/04-runtime-config.md new file mode 100644 index 0000000..34e8b1d --- /dev/null +++ b/.planning/phase-1-foundation/04-runtime-config.md @@ -0,0 +1,105 @@ +# Task 1.4 β€” Runtime config endpoint + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.2 +**Wiki refs:** `docs/wiki/entities/react-spa.md` Β§Auth pattern; `.planning/ROADMAP.md` design rule 7 + +## Goal + +Establish the runtime-config pattern: a `/config.json` file fetched at app boot, validated with zod, made available via a typed React context. Per-environment values (Directus URL, processor WS URL, Google Maps key, feature flags) live there β€” not in build-time `import.meta.env`. One built image, many deploy environments. + +## Deliverables + +- `src/config/schema.ts` β€” zod schema: + ```ts + export const RuntimeConfigSchema = z.object({ + directusUrl: z.string().url(), // public-facing Directus URL (e.g. https://stage.trmtracking.org/api) + liveWsUrl: z.string().url(), // public-facing Processor WS URL (wss://stage.trmtracking.org/ws-live) + businessWsUrl: z.string().url(), // public-facing Directus WS URL (wss://stage.trmtracking.org/ws-business) + googleMapsKey: z.string().optional(), // BYOK; if absent, Google tile sources are hidden in the UI + env: z.enum(['dev', 'stage', 'prod']), // for log labels and feature flag conditionals + }); + export type RuntimeConfig = z.infer; + ``` +- `src/config/load.ts`: + - `async function loadConfig(): Promise` β€” fetches `/config.json`, parses with zod, throws on validation failure. + - On dev, the file is served directly from `public/config.json` by Vite. On stage/prod, the proxy serves it (Traefik file-middleware or a tiny nginx rule). +- `src/config/context.tsx`: + - `RuntimeConfigProvider` β€” fetches config in a `useEffect` on mount, holds it in state, passes via context. + - `useRuntimeConfig()` hook β€” throws if used outside the provider, returns the typed config. + - While loading, render a minimal "Loading…" placeholder (one centred div, no shadcn dependency). + - On error, render an error state with the validation issues β€” clear enough that a misconfigured `config.json` doesn't masquerade as a generic crash. +- `public/config.json` β€” checked-in file with dev defaults: + ```json + { + "directusUrl": "/api", + "liveWsUrl": "/ws-live", + "businessWsUrl": "/ws-business", + "env": "dev" + } + ``` + Note: relative URLs work because the SPA always runs same-origin (Vite dev proxy or Traefik in stage/prod). Document this in the file's accompanying README. +- `src/main.tsx` updated to wrap `` in ``. +- `README.md` updated with a "Runtime config" section explaining: where the file lives in dev vs stage/prod, the schema, how to add a new field, the dev defaults. + +## Specification + +### Why runtime config not build-time + +Build-time means one bundle per environment. That breaks reproducibility (dev image β‰  stage image β‰  prod image), prevents promoting a known-good build to prod ("ship the stage build to prod after stage soak"), and forces a rebuild for any env-only change (add a feature flag β†’ rebuild β†’ reupload). Runtime config lets one image serve all environments β€” the proxy or stack overrides the file. + +The Directus-side runtime config (`DIRECTUS_PUBLIC_URL`, etc.) and Processor-side (`DIRECTUS_BASE_URL`, etc.) follow the same pattern. SPA consistency. + +### How the file is overridden in stage/prod + +Two reasonable options: + +**Option A: Static file in the SPA's nginx serve config.** The Dockerfile (1.9) bakes a default `config.json` into the image. Override happens at deploy time: a Traefik file-middleware (or a sibling nginx rule) intercepts `/config.json` and serves a different file from a host volume. Requires zero rebuilds. + +**Option B: Server-rendered config.** Directus could serve `/config.json` via a custom endpoint that reads env vars. Rebuildable through Directus. + +**Pick Option A.** Simpler, zero new code, deploy-time override is clean. Document the override pattern in `trm/deploy/README.md` (1.10). + +### What goes in config vs what doesn't + +In config: + +- URLs the SPA calls (Directus, Processor WS, business WS). +- Optional API keys the operator brings (Google Maps). +- Environment label for log/feature use. + +NOT in config: + +- Anything that varies per-user (the user's role, their subscriptions). +- Anything operational secret (no JWT signing keys, no DB passwords). +- Anything that changes during the session (that's state, not config). + +### Failure modes + +- **`config.json` missing (404):** error state with "Runtime config not found at /config.json. Did the deployment skip its config volume?" Don't fall back to defaults silently β€” that hides misconfiguration. +- **`config.json` invalid (zod fails):** error state listing the validation issues. Useful for catching typos in stage configs. +- **Network failure on first load:** retry once after 500ms, then show error. Don't infinite-retry β€” the user can reload the page if the network heals. + +### Why a context provider rather than a Zustand store + +Auth state needs Zustand (read in many places, granular subscribers). Runtime config is read once and never changes during the session β€” context is exactly right for that. Don't over-engineer. + +## Acceptance criteria + +- [ ] `pnpm dev` shows a brief "Loading…" then renders the placeholder content. +- [ ] Manually editing `public/config.json` to invalid shape and reloading shows the error state with the validation issues, not a blank screen. +- [ ] Manually editing `public/config.json` to invalidate URL format (e.g. `directusUrl: "not-a-url"`) shows the same error state. +- [ ] `useRuntimeConfig()` is callable from any descendant of `` and returns typed config. +- [ ] Calling `useRuntimeConfig()` outside the provider throws a clear error (not "cannot read property X of undefined"). +- [ ] The default `public/config.json` validates against the schema. + +## Risks / open questions + +- **Same-origin assumption.** The relative URLs (`/api`, `/ws-live`) only work if everything is same-origin. If a deploy environment ever wants to point the SPA at a different-origin Directus (e.g. for a partner integration), the config schema accepts absolute URLs β€” but cookie auth breaks at that point. Document the constraint. +- **Field projection in zod.** If we ever add a sensitive field (a per-tenant analytics key), it would be visible to anyone hitting `/config.json` directly. Don't put anything sensitive there. The schema's `RuntimeConfigSchema.optional()` discipline helps but doesn't enforce. +- **Hot reload of config in dev.** Editing `public/config.json` requires a hard reload (Vite watches the file but the React Provider only fetches on mount). Acceptable for dev. + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/05-directus-auth-client.md b/.planning/phase-1-foundation/05-directus-auth-client.md new file mode 100644 index 0000000..576aa21 --- /dev/null +++ b/.planning/phase-1-foundation/05-directus-auth-client.md @@ -0,0 +1,208 @@ +# Task 1.5 β€” Directus auth client (cookie mode + refresh) + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.4 +**Wiki refs:** `docs/wiki/entities/directus.md`; `docs/wiki/entities/react-spa.md` Β§Auth pattern; `docs/wiki/synthesis/processor-ws-contract.md` Β§Auth handshake + +## Goal + +Wire the Directus SDK with cookie-mode authentication: `/auth/login` issues an httpOnly refresh cookie + returns an access token (held in memory only); `/auth/refresh` rotates both; the SDK auto-refreshes on 401. Build a small Zustand auth store (`user` + `status`) so any component can react to login/logout. After this task, the auth machinery is functional but headless β€” the login page (1.6) and route guard (1.7) build on it. + +## Deliverables + +- `src/auth/client.ts` β€” typed Directus SDK client: + + ```ts + import { authentication, createDirectus, rest } from '@directus/sdk'; + import { config } from '@/config/context'; // see "Bootstrapping" below + + export type DirectusUser = { + id: string; + email: string | null; + role: string | null; + first_name: string | null; + last_name: string | null; + }; + + export type Schema = { + // grow as collections become typed; for now an empty schema + }; + + export const directus = createDirectus(config.directusUrl) + .with(authentication('cookie', { credentials: 'include', autoRefresh: true })) + .with(rest({ credentials: 'include' })); + ``` + + - The `'cookie'` mode + `credentials: 'include'` is the SDK's contract for httpOnly-refresh-cookie + in-memory-access-token. `autoRefresh: true` enables the SDK's built-in 401 interceptor. + +- `src/auth/store.ts` β€” Zustand auth store: + + ```ts + type AuthState = + | { status: 'unknown' } // initial; before /users/me check + | { status: 'anonymous' } + | { status: 'authenticated'; user: DirectusUser } + | { status: 'authenticating' }; // login in progress + + type AuthActions = { + initialize(): Promise; // call /users/me; set status accordingly + login(email: string, password: string): Promise<{ ok: true } | { ok: false; error: string }>; + logout(): Promise; + setUser(user: DirectusUser): void; // for after refresh sees a user-update + }; + ``` + + - `initialize` is called once at app boot from ``'s post-load step. It tries `/users/me`; on 200, sets `status: 'authenticated'`; on 401/error, sets `status: 'anonymous'`. + +- `src/auth/guard.ts` β€” `useRequireAuth()` hook used by Phase 1.7's protected routes: + ```ts + export function useRequireAuth() { + const status = useAuthStore((s) => s.status); + const navigate = useNavigate(); // TanStack Router; lands in 1.7 + useEffect(() => { + if (status === 'anonymous') navigate({ to: '/login' }); + }, [status, navigate]); + return status; + } + ``` +- `src/auth/index.ts` β€” barrel re-exports. +- `src/main.tsx` β€” call `useAuthStore.getState().initialize()` after the runtime config loads. (More precisely: the `RuntimeConfigProvider`'s success path triggers `initialize()` exactly once.) + +## Specification + +### Why a Zustand auth store + +The auth state is read in many places: route guards, the user-menu component, audit logs, the WS client (when 1.5 wires the WS). React Context cascades re-renders to all consumers on every change; Zustand's selector-based subscription means only components that read the changed slice re-render. For auth (small, infrequent changes) the difference is small in practice, but the pattern is the same one we'll use for live-position state in Phase 2 β€” staying consistent is the win. + +### Why cookie mode (not bearer-token mode) + +Three reasons documented in `docs/wiki/entities/react-spa.md` Β§Auth pattern: + +1. **No XSS exfiltration of long-lived credentials.** The refresh cookie is `httpOnly`; JS can never read it. +2. **WS upgrade carries the cookie automatically.** Same-origin β†’ browser attaches it. No "send the JWT in the upgrade message" dance. +3. **Same pattern Directus already supports.** No custom server work; just the right login mode. + +The access token is held in memory by the SDK (in `directus._authentication.tokens` internally; not directly accessed by app code). It expires (default 15 min); the SDK's `autoRefresh: true` calls `/auth/refresh` on the next request that hits a 401, transparently. + +### Bootstrapping order + +``` +1. main.tsx renders shell. +2. RuntimeConfigProvider fetches /config.json. +3. On success: imports the config singleton, runtime-config-driven createDirectus(...) is realisable. +4. RuntimeConfigProvider calls useAuthStore.getState().initialize(). +5. initialize() calls directus.request(readMe()) (the SDK's /users/me wrapper). +6. On 200 β†’ setUser, status: 'authenticated'. +7. On 401 β†’ status: 'anonymous'. +8. Provider's children render β€” they can now read the auth store. +``` + +The order matters: createDirectus needs the runtime config to know what URL to hit. So the SDK client can't be a top-level module-level `const`; it has to be created after config loads. Two ways to handle this: + +**Option A: Lazy creation.** `getDirectusClient()` returns the singleton, creating it on first call. The first call lands inside `initialize()`, after the config has resolved. + +**Option B: Module-level after config import.** The config context exposes a `directus` instance that's only valid after the provider has loaded. Requires the import chain to defer. + +**Pick Option A.** Cleaner β€” the singleton creation is explicitly tied to a function call rather than module-load order. + +```ts +// src/auth/client.ts +let _client: ReturnType | null = null; + +export function getDirectus() { + if (!_client) { + const cfg = useRuntimeConfig.getState(); // or pass via param; see below + _client = createDirectus(cfg.directusUrl) + .with(authentication('cookie', { credentials: 'include', autoRefresh: true })) + .with(rest({ credentials: 'include' })); + } + return _client; +} +``` + +If the runtime config hook isn't easily accessible from outside React, expose the config as a Zustand store too (or read directly from the loaded JSON via a non-React module). The simplest pragmatic answer: the config provider sets a module-level global on success, and `getDirectus()` reads it. + +### `initialize()` β€” the one-time login check + +```ts +async initialize() { + const directus = getDirectus(); + set({ status: 'authenticating' }); // tiny window; UX-invisible + try { + const user = await directus.request(readMe({ + fields: ['id', 'email', 'role', 'first_name', 'last_name'], + })); + set({ status: 'authenticated', user: user as DirectusUser }); + } catch (err) { + // 401 from /users/me means no session. Anything else is unusual but treat the same. + set({ status: 'anonymous' }); + } +} +``` + +The `try/catch` swallowing all errors is deliberate: at boot, a failure to read `/users/me` _is_ the "anonymous" state. We don't surface boot-time errors here. + +### `login(email, password)` + +```ts +async login(email, password) { + const directus = getDirectus(); + set({ status: 'authenticating' }); + try { + await directus.login(email, password, { mode: 'cookie' }); + // After login the SDK has the access token; re-fetch user. + const user = await directus.request(readMe({ fields: [...] })); + set({ status: 'authenticated', user: user as DirectusUser }); + return { ok: true }; + } catch (err) { + set({ status: 'anonymous' }); + return { ok: false, error: humanizeAuthError(err) }; + } +} +``` + +`humanizeAuthError` maps Directus's error codes (`INVALID_CREDENTIALS`, `INVALID_OTP`, etc.) to user-friendly strings. Default for unknown: "Login failed. Please try again." + +### `logout()` + +```ts +async logout() { + const directus = getDirectus(); + try { + await directus.logout(); + } catch { + // Ignore β€” even if logout fails server-side, clear local state. + } + set({ status: 'anonymous' }); + // 1.8 builds on this: also invalidate the TanStack Query cache. +} +``` + +### What this task does NOT do + +- **No login page.** That's 1.6. +- **No route guards wired into routes.** That's 1.7. +- **No automatic redirect on logout.** The component calling `logout()` decides where to navigate. 1.8 wires the actual logout button into the app shell. +- **No "remember me."** Directus's refresh cookie is the persistence; the SPA doesn't add another layer. + +## Acceptance criteria + +- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm format:check` clean. +- [ ] In a browser console (with Directus running locally + a known user seeded): `useAuthStore.getState().login('admin@example.com', 'password')` resolves to `{ ok: true }` and `useAuthStore.getState().status === 'authenticated'`. +- [ ] After login, `document.cookie` shows nothing (refresh cookie is httpOnly β€” invisible to JS, as intended). +- [ ] After login, `useAuthStore.getState().user` is the expected user record. +- [ ] `useAuthStore.getState().logout()` resolves; status returns to `'anonymous'`. +- [ ] Wrong credentials produce `{ ok: false, error: 'Invalid email or password.' }` (or similar humanised string). +- [ ] Network error during login resolves to `{ ok: false, error: ... }` β€” no uncaught exception bubbling to the React error boundary. + +## Risks / open questions + +- **`@directus/sdk` cookie mode quirks.** The SDK changed cookie-mode handling between 11.0 and 11.x. Verify the exact API against the version installed in 1.2 (`pnpm list @directus/sdk`). Adjust the `authentication('cookie', ...)` call signature if needed. +- **CSRF.** Directus's cookie mode includes CSRF protection (the access token in the response body is the anti-CSRF measure β€” readable by JS, not by a third-party form post). The SDK handles this; we don't add custom CSRF headers. +- **Session persistence across reloads.** The httpOnly refresh cookie persists across page reloads; on each load `initialize()` calls `/users/me`, which does a transparent refresh if needed, and the user is "still logged in." Verify in the integration smoke (1.6 task acceptance). +- **Lazy `getDirectus()` and HMR.** During Vite dev HMR, the module reloads but the singleton survives (it's in a closure). If the config changes mid-session this becomes stale; acceptable trade-off β€” config rarely changes in dev. + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/06-login-page.md b/.planning/phase-1-foundation/06-login-page.md new file mode 100644 index 0000000..3b8b388 --- /dev/null +++ b/.planning/phase-1-foundation/06-login-page.md @@ -0,0 +1,163 @@ +# Task 1.6 β€” Login page + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.5 +**Wiki refs:** `docs/wiki/entities/react-spa.md` Β§Auth pattern + +## Goal + +A clean, single-screen login page. Email + password fields, validation via zod + react-hook-form, submit calls `useAuthStore.login(...)`, error state surfaces a humanised message under the form, success redirects to the intended destination (or `/` if none). + +The look-and-feel target is "professional, not flashy" β€” race operators logging in on stage day shouldn't be wondering whether the page is broken. Use shadcn/ui's `Card` + `Input` + `Label` + `Button` + `Alert` primitives installed in 1.2. + +## Deliverables + +- `src/ui/pages/login.tsx`: + - A centred card on a plain background. + - "TRM" or "Time Racing Management" header (whatever brand text is agreed; can be a placeholder for now). + - Email field (type `email`, autocomplete `username`). + - Password field (type `password`, autocomplete `current-password`). + - "Sign in" button. + - Validation: email must be valid format; password must be β‰₯ 1 character (Directus enforces real password rules; we don't duplicate them client-side). + - Submit handler calls `useAuthStore.getState().login(email, password)`. While the call is in flight, the button shows "Signing in…" and is disabled. + - On `{ ok: false, error }`, render an `` above the form with the error message. + - On `{ ok: true }`, navigate to `searchParams.get('redirect') ?? '/'`. +- `src/routes/login.tsx` (TanStack Router file-based route β€” final wiring lands in 1.7, but the page component itself is built here): + - Exports the page component. + - Accepts a `redirect` search param. + - When the auth store transitions to `'authenticated'` (e.g. via concurrent tab login), auto-navigate to `redirect` to avoid a stale login page. +- `src/ui/pages/__tests__/login.test.tsx` (optional for Phase 1 β€” Vitest infra lands in Phase 3; for now a manual smoke is acceptance enough). + +## Specification + +### Form library + validation + +```tsx +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +const LoginFormSchema = z.object({ + email: z.string().email({ message: 'Enter a valid email address.' }), + password: z.string().min(1, { message: 'Password is required.' }), +}); +type LoginForm = z.infer; + +const form = useForm({ + resolver: zodResolver(LoginFormSchema), + defaultValues: { email: '', password: '' }, +}); +``` + +Field-level errors render under each input. Submit-level errors (the "wrong credentials" message) render in the alert above the form. + +### Layout + +```tsx +
+ + + Sign in to TRM + Use your operator credentials. + + + {submitError && ( + + {submitError} + + )} +
+
+ + + {form.formState.errors.email && ( +

{form.formState.errors.email.message}

+ )} +
+
+ + + {form.formState.errors.password && ( +

+ {form.formState.errors.password.message} +

+ )} +
+ +
+
+
+
+``` + +### Submit handler + +```tsx +const [submitError, setSubmitError] = useState(null); +const [isSubmitting, setIsSubmitting] = useState(false); +const navigate = useNavigate(); +const { redirect } = useSearch({ from: '/login' }); + +async function onSubmit(values: LoginForm) { + setSubmitError(null); + setIsSubmitting(true); + const result = await useAuthStore.getState().login(values.email, values.password); + setIsSubmitting(false); + if (result.ok) { + navigate({ to: redirect ?? '/', replace: true }); + } else { + setSubmitError(result.error); + } +} +``` + +`replace: true` on success β€” don't pile login pages in the back-button history. + +### Auto-navigate if already authenticated + +If the user is already authenticated (e.g. they bookmarked `/login` and came back), redirect them away. Watch the auth store: + +```tsx +const status = useAuthStore((s) => s.status); +useEffect(() => { + if (status === 'authenticated') { + navigate({ to: redirect ?? '/', replace: true }); + } +}, [status, navigate, redirect]); +``` + +### What this task does NOT include + +- **Forgot-password / password-reset link.** Directus has `/auth/password/request` but the email-template + the page that handles the reset link are out of Phase 1 scope. Add a placeholder "Forgot password?" link that's `disabled` or routes to a "Not yet available" page; document as deferred. +- **OAuth / SSO buttons.** Directus supports OAuth, but Phase 1's dogfood doesn't need it. Skip entirely. +- **2FA / OTP.** Same β€” not needed for the dogfood. If a Directus user has 2FA enabled, login will fail with a specific error code; add a TODO comment, document, and ignore for now. +- **"Remember me" checkbox.** The refresh cookie is the persistence layer; no separate "remember me" toggle. + +## Acceptance criteria + +- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm format:check` clean. +- [ ] `pnpm dev` β†’ navigate to `/login` (after 1.7 wires routing) β†’ renders the form. +- [ ] Submitting with empty fields shows field-level validation errors; no network call made. +- [ ] Submitting with valid format but wrong credentials shows the alert with a humanised error. +- [ ] Submitting with correct credentials redirects to `/` (or to the `?redirect=...` param if present). +- [ ] Already-authenticated user landing on `/login` is redirected away, not stuck on the page. +- [ ] Form is keyboard-navigable (tab order: email β†’ password β†’ submit); pressing Enter in either field submits. +- [ ] Browser autofill works (the autocomplete attributes let password managers populate correctly). + +## Risks / open questions + +- **Form a11y.** The shadcn `Form` primitive (different from raw `
`) wires error announcements via `aria-describedby` automatically. Use it if 1.2 added it; otherwise the manual error rendering above is acceptable for v1. +- **Visual design.** This is functional, not branded. If the user wants a real visual identity before dogfood, that's a Phase 3 task ("apply visual brand"). For now, plain card on muted background. +- **Error message localisation.** English-only; i18n is Phase 4. + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/07-routing-skeleton.md b/.planning/phase-1-foundation/07-routing-skeleton.md new file mode 100644 index 0000000..313f84b --- /dev/null +++ b/.planning/phase-1-foundation/07-routing-skeleton.md @@ -0,0 +1,222 @@ +# Task 1.7 β€” Routing skeleton (TanStack Router + role-aware guards) + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.5, 1.6 +**Wiki refs:** `docs/wiki/entities/react-spa.md`; `.planning/ROADMAP.md` design rule 6 + +## Goal + +Wire TanStack Router with file-based routes: a root layout, a public `/login` route, a protected route group (everything else), and a role-aware guard that gates routes by `user.role`. After this task, an unauthenticated user is redirected to `/login`; an authenticated user lands on a placeholder home page. + +The role-aware guard is in shape from day one even though everyone's effectively admin until [[directus]] Phase 4 β€” retrofitting "the SPA assumes everyone sees everything" later is painful. + +## Deliverables + +- `vite.config.ts` updated to include `@tanstack/router-plugin/vite`: + ```ts + import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; + // ... in plugins array, BEFORE react(): + TanStackRouterVite({ target: 'react', autoCodeSplitting: true }), + ``` +- `src/routeTree.gen.ts` (auto-generated by the plugin; gitignored). +- `src/routes/__root.tsx` β€” the root route: + - Wraps everything in a layout (currently just an `` for the matched child). + - Provides the TanStack Query `QueryClientProvider`. + - In dev: `` + `` (lazy-loaded so they don't ship in prod). + - Reads the auth store and redirects to `/login` if status is `'anonymous'` and the matched route is in the protected group (the `_authed` segment). +- `src/routes/login.tsx` β€” the public login route. Renders `` from 1.6. +- `src/routes/_authed/route.tsx` β€” the protected route group. The layout for any route under `_authed/`. Calls `useRequireAuth()` from 1.5; if status is anything other than `'authenticated'`, returns a loading placeholder; otherwise renders ``. +- `src/routes/_authed/index.tsx` β€” placeholder home page (`/`). Just a header "TRM" + "Logged in as {user.first_name}" + a placeholder card "Live monitoring map will land in Phase 2." Logout button (Phase 1.8 wires the action; this task adds the button). +- `src/App.tsx` β€” replaced. Now just renders the ``: + + ```tsx + import { RouterProvider, createRouter } from '@tanstack/react-router'; + import { routeTree } from './routeTree.gen'; + import { queryClient } from './lib/query-client'; + + const router = createRouter({ + routeTree, + context: { queryClient }, + defaultPreload: 'intent', + }); + + declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } + } + + export default function App() { + return ; + } + ``` + +- `src/lib/query-client.ts` β€” module-level `QueryClient` singleton. +- Role-aware guard: + - `useRequireRole(allowedRoles: string[])` β€” used inside protected route loaders/components. Currently no route restricts by role (everyone's admin); the helper exists and is documented for Phase 2 onwards. +- `package.json` `dependencies` already include the TanStack Router/Query packages from 1.2; ensure devtools are added: `pnpm add -D @tanstack/router-plugin @tanstack/router-devtools @tanstack/react-query-devtools`. + +## Specification + +### File-based routing layout + +TanStack Router's file-based routing maps `src/routes/` to URL paths. Naming conventions: + +| File | Path | Purpose | +| -------------------- | ---------------------- | ----------------------------------------------------------------------------- | +| `__root.tsx` | (root layout) | Wraps everything; provides QueryClient and runs the auth gate | +| `login.tsx` | `/login` | Public login page | +| `_authed/route.tsx` | (layout for `_authed`) | Protected layout; the `_` prefix is "pathless" β€” no URL segment for `_authed` | +| `_authed/index.tsx` | `/` | Home page (matches the empty path under `_authed`) | +| `_authed/events.tsx` | `/events` | Future event-list page (Phase 2-ish) | + +The pathless `_authed` group is the TanStack Router idiom for "share a layout (and a guard) without adding a URL segment." All authenticated routes live under it; all live-public routes are siblings (just `login.tsx` for now). + +### Root route β€” auth gate + +```tsx +// src/routes/__root.tsx +import { createRootRouteWithContext, Outlet, redirect } from '@tanstack/react-router'; +import { useAuthStore } from '@/auth/store'; +import type { QueryClient } from '@tanstack/react-query'; + +export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ + component: RootLayout, +}); + +function RootLayout() { + return ( + <> + + {import.meta.env.DEV && } + + ); +} +``` + +The actual auth gate lives in `_authed/route.tsx` so the public `/login` route is unaffected. + +### Protected layout + +```tsx +// src/routes/_authed/route.tsx +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; +import { useAuthStore } from '@/auth/store'; + +export const Route = createFileRoute('/_authed')({ + beforeLoad: ({ location }) => { + const status = useAuthStore.getState().status; + if (status === 'unknown' || status === 'authenticating') { + // Still resolving; let the component render a loading state. + return; + } + if (status === 'anonymous') { + throw redirect({ to: '/login', search: { redirect: location.href } }); + } + }, + component: AuthedLayout, +}); + +function AuthedLayout() { + const status = useAuthStore((s) => s.status); + if (status !== 'authenticated') { + return ; + } + return ; +} +``` + +The `beforeLoad` redirect is the canonical TanStack Router idiom. `search.redirect = location.href` lets `/login` send the user back where they tried to go. + +### Why also a runtime check inside the component + +`beforeLoad` runs once per navigation; if the auth state changes during the session (logout in another tab β†’ status becomes `'anonymous'`), the existing route doesn't auto-navigate. The runtime check + the `useRequireAuth` hook from 1.5 catch that case. Belt-and-braces. + +### Role-aware guard helper + +```ts +export function useRequireRole(allowedRoles: string[]) { + const user = useAuthStore((s) => (s.status === 'authenticated' ? s.user : null)); + const navigate = useNavigate(); + useEffect(() => { + if (!user) return; // not authenticated β€” _authed layout already handles + if (user.role && !allowedRoles.includes(user.role)) { + navigate({ to: '/', replace: true }); // bounce to home + } + }, [user, allowedRoles, navigate]); +} +``` + +Currently no Phase 1 route uses it. Document with an example in the file comment so future task implementers know to add `useRequireRole(['race-director'])` to admin-only pages when those land in Phase 2+. + +### Home page placeholder + +```tsx +// src/routes/_authed/index.tsx +import { createFileRoute } from '@tanstack/react-router'; +import { useAuthStore } from '@/auth/store'; +import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card'; +import { Button } from '@/ui/primitives/button'; + +export const Route = createFileRoute('/_authed/')({ + component: HomePage, +}); + +function HomePage() { + const user = useAuthStore((s) => (s.status === 'authenticated' ? s.user : null)); + if (!user) return null; // covered by layout + + return ( +
+
+

TRM

+
+ {user.first_name ?? user.email} + +
+
+ + + Live monitoring map + + +

+ The live-position map lands in Phase 2 once the Processor's WS endpoint is shipped. +

+
+
+
+ ); +} +``` + +### What this task does NOT do + +- **Logout action.** Button is rendered but the click handler is wired in 1.8. +- **Sub-routes for events/devices/etc.** Those land in Phase 2 alongside the live map. +- **Full app shell with sidebar nav.** The header above is intentionally minimal β€” Phase 2 expands it when there are real navigation targets. + +## Acceptance criteria + +- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm format:check` clean. +- [ ] `pnpm dev` β†’ navigating to `/` while logged out redirects to `/login?redirect=...`. +- [ ] After successful login, the SPA navigates to the redirect target (or `/` if none). +- [ ] Refreshing on `/` while authenticated stays on `/`; the auth store re-initialises against the persisted refresh cookie and `initialize()` resolves to `'authenticated'` before the route gate fires. +- [ ] Refreshing on `/` while unauthenticated redirects to `/login`. +- [ ] Browser back/forward buttons work between `/login` and `/` correctly (no infinite redirect loops). +- [ ] Devtools render in dev mode (`import.meta.env.DEV === true`); they don't render in prod build (`pnpm build` then `pnpm preview`). +- [ ] Type-checked navigation: `` is OK; `` fails type-check. + +## Risks / open questions + +- **Race between `initialize()` and `beforeLoad`.** If `beforeLoad` runs before the auth store has finished `initialize()`, status is `'unknown'` and the gate falls through to the component (which shows the loading spinner). Verify the timing β€” TanStack Router's `defaultPendingComponent` may be a cleaner place for the loading state. +- **TanStack Router file-based routes regenerate on save.** The `routeTree.gen.ts` file is auto-managed; gitignore it. CI will regenerate during `pnpm build`. Document in README. +- **Search params type-safety.** TanStack Router's typed search params require a validation function. For `/login`, validate `redirect` as `z.string().optional()`. Add as a small `validateSearch` on the login route. +- **Devtools bundle size.** Lazy-load via dynamic import inside the `import.meta.env.DEV` branch so the prod bundle doesn't ship them. + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/08-logout-flow.md b/.planning/phase-1-foundation/08-logout-flow.md new file mode 100644 index 0000000..9ae69f3 --- /dev/null +++ b/.planning/phase-1-foundation/08-logout-flow.md @@ -0,0 +1,134 @@ +# Task 1.8 β€” Logout flow + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.7 +**Wiki refs:** `docs/wiki/entities/react-spa.md` + +## Goal + +Wire the "Sign out" button rendered by 1.7's home page to a real logout flow: call `useAuthStore.logout()`, invalidate the TanStack Query cache, navigate to `/login`. Small task, but worth its own file because it touches three subsystems and has subtle requirements (cache invalidation, race with concurrent calls). + +After this task the Phase 1 happy path is end-to-end: log in β†’ see home β†’ log out β†’ back to login. + +## Deliverables + +- `src/auth/logout.ts` β€” orchestration helper: + ```ts + export async function performLogout(opts: { queryClient: QueryClient; navigate: NavigateFn }) { + await useAuthStore.getState().logout(); + opts.queryClient.clear(); + await opts.navigate({ to: '/login', replace: true }); + } + ``` +- `src/ui/components/logout-button.tsx` β€” small shadcn `Button` wrapping the call: + + ```tsx + export function LogoutButton({ variant = 'outline' }: Props) { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const [isLoggingOut, setIsLoggingOut] = useState(false); + + async function onClick() { + if (isLoggingOut) return; + setIsLoggingOut(true); + try { + await performLogout({ queryClient, navigate }); + } catch { + // Even if logout fails, the store state is now anonymous; the navigate has happened. + // Nothing useful to do beyond logging. + } + } + + return ( + + ); + } + ``` + +- `src/routes/_authed/index.tsx` updated to use `` instead of the placeholder button from 1.7. +- Cross-tab logout handling β€” `src/auth/cross-tab-sync.ts`: + - Subscribes to `window.addEventListener('storage', ...)` for a `trm-auth-version` key. + - On change, re-runs `useAuthStore.getState().initialize()` to pick up the new state. + - Mounted once at app boot from ``'s success path (alongside the initial `initialize()` call). + +## Specification + +### Why clear the query cache on logout + +After logout, the next user (someone else logging in on the same browser) shouldn't see cached data the previous user pulled. TanStack Query's cache is keyed by query keys, not by user identity β€” without an explicit clear, the next login would briefly show stale data while refetches resolve. `queryClient.clear()` removes everything; subsequent queries will refetch fresh. + +The Zustand auth store also resets to `'anonymous'`. No user data leaks across sessions in process memory. + +### Why a separate orchestration helper + +Three things have to happen in order: server-side logout, client-side cache clear, navigation. Wrapping them in a function (`performLogout`) means: + +1. Components call one thing. +2. The order is documented in code, not in the component. +3. If we ever add another step (e.g. closing the live WS in Phase 2), it lands in one place. + +### Cross-tab sync + +Pattern: when a user logs out in one tab, other tabs should also lose their session. Two ways: + +**Option A: BroadcastChannel.** Modern API; the auth store posts on `logout`/`login`; other tabs listen. Cleaner. + +**Option B: localStorage + storage event.** Older but universal. Write a "version" key on auth events; other tabs see the storage event and re-init. + +**Pick B for now.** Slightly broader browser support; simpler. Migrate to BroadcastChannel later if it becomes preferred. + +```ts +// src/auth/cross-tab-sync.ts +const VERSION_KEY = 'trm-auth-version'; + +export function startCrossTabSync() { + // After every login/logout in this tab, bump the version so other tabs see a storage event. + useAuthStore.subscribe((state, prev) => { + if (state.status !== prev.status) { + localStorage.setItem(VERSION_KEY, String(Date.now())); + } + }); + + // React to bumps from other tabs. + window.addEventListener('storage', (ev) => { + if (ev.key !== VERSION_KEY) return; + useAuthStore.getState().initialize(); + }); +} +``` + +Worth flagging a subtlety: writing to localStorage doesn't trigger the storage event in the _same_ tab. So this works exactly right β€” bump in tab A, tab B sees it. + +### What logout does NOT do + +- **No "Are you sure?" confirmation.** Click and out. Race operators clicking accidentally is unlikely in stage day operation; the nuisance of a confirmation modal isn't worth the marginal protection. +- **No server-side session enumeration / kill all sessions.** Directus's `/auth/logout` invalidates the current session's refresh token. Other concurrent sessions (other browsers, other devices) keep working. If a "log out everywhere" feature is needed, that's a Phase 4 idea. +- **No analytics event.** No analytics platform yet. + +### What happens if `directus.logout()` fails + +`useAuthStore.logout()` (from 1.5) already swallows the error and forces local state to `'anonymous'`. So the user is "logged out from the SPA" even if the server call failed (e.g. network outage). The refresh cookie may still be valid server-side, but without local state to replay it the SPA can't reuse it. On next login attempt, Directus may overwrite or invalidate the dangling cookie naturally. + +This is the right trade-off: logout that fails should _not_ leave the user appearing logged in. + +## Acceptance criteria + +- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm format:check` clean. +- [ ] Click "Sign out" β†’ button shows "Signing out…" briefly β†’ SPA navigates to `/login` β†’ refresh cookie is gone (verifiable: refreshing the page now stays on `/login` instead of going to `/`). +- [ ] Logging in twice (in two tabs) and logging out in tab A β†’ tab B's auth state transitions to `'anonymous'` (verifiable by watching the route gate redirect tab B to `/login` on next interaction). +- [ ] After logout, opening the React Query devtools shows an empty cache. +- [ ] Logout while offline (DevTools "Network: offline"): button completes, navigates to `/login`, no UI hang. Refreshing the page stays on `/login`. +- [ ] Double-click on the logout button doesn't cause a duplicate `/auth/logout` call (the `isLoggingOut` flag guards). + +## Risks / open questions + +- **Refresh-cookie zombie.** If `directus.logout()` fails server-side, the refresh cookie may still be valid. On next page load, `initialize()` calls `/users/me`, which triggers a silent refresh, and the user is logged back in transparently. To force complete logout, the SPA should _also_ issue a request to clear the cookie locally before navigating. Test this against stage and document the behaviour. +- **Cross-tab sync race.** If tab A logs out and tab B is mid-request, tab B's request might 401 mid-flight. Handle gracefully: TanStack Query's default error handling shows a toast / retry; the route gate then redirects to login. Acceptable for v1. +- **Multiple BroadcastChannel listeners on hot-reload.** During Vite dev HMR, `startCrossTabSync()` may run multiple times. Guard with a module-level boolean to ensure single registration. + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/09-gitea-ci-and-dockerfile.md b/.planning/phase-1-foundation/09-gitea-ci-and-dockerfile.md new file mode 100644 index 0000000..d135c06 --- /dev/null +++ b/.planning/phase-1-foundation/09-gitea-ci-and-dockerfile.md @@ -0,0 +1,192 @@ +# Task 1.9 β€” Gitea CI + Dockerfile + nginx static serve + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.2 (need build to be working) +**Wiki refs:** `docs/wiki/entities/react-spa.md`; `trm/processor/.gitea/workflows/build.yml` and `trm/directus/.gitea/workflows/build.yml` for pattern alignment + +## Goal + +Build a Docker image of the SPA (static bundle served by nginx), publish to the Gitea container registry on every push to `main`, matching the CI conventions established by `trm/processor` and `trm/directus`. Include lint + typecheck + format-check + build as the gate before image publish. + +After this task, pushing to `main` produces a `git.dev.microservices.al/trm/spa:main` image that `trm/deploy` can pull. + +## Deliverables + +- `Dockerfile`: + - Multi-stage: `builder` (node:22-alpine, install deps with pnpm, run build) β†’ `runner` (nginx:alpine, copy `dist/` to `/usr/share/nginx/html`). + - BuildKit cache mounts for `pnpm fetch` + `pnpm install --offline` (matching the processor's pattern). + - `nginx.conf` baked in (next bullet). + - Listen on `:80`; the deploy proxy fronts it. +- `nginx.conf`: + - Single `server` block listening on `:80`. + - Static-serve `/usr/share/nginx/html` with `try_files $uri $uri/ /index.html;` (SPA routing fallback so `/login` etc. all serve `index.html`). + - `location = /config.json` block: serve from a separate path that can be volume-mounted in stage/prod (the override path described in 1.4). Default value is the baked-in dev defaults. + - `gzip` on for `text/css`, `application/javascript`, `application/json`. (Brotli later if it becomes a concern.) + - Cache headers: `index.html` no-cache; everything else (the hashed JS/CSS/sprite assets) `Cache-Control: public, max-age=31536000, immutable`. +- `.dockerignore`: + - Includes `node_modules`, `dist`, `.git`, `.gitea`, `.planning`, `*.md` other than necessary, etc. Match the existing pattern from processor/directus. +- `.gitea/workflows/build.yml`: + - Triggers: push to `main`, push to `phase-2-*` branches. + - Path filter: `src/**`, `public/**`, `*.json`, `*.ts`, `*.tsx`, `*.js`, `*.cjs`, `*.html`, `Dockerfile`, `nginx.conf`, `.gitea/workflows/build.yml`. Skip on docs-only changes. + - Single job; mirrors `trm/processor/.gitea/workflows/build.yml` structure. + - Steps: + 1. Checkout. + 2. Setup pnpm (use the same caching strategy as the processor). + 3. `pnpm install --frozen-lockfile`. + 4. `pnpm typecheck`. + 5. `pnpm lint`. + 6. `pnpm format:check`. + 7. `pnpm build`. + 8. Build Docker image (use the BuildKit cache between runs). + 9. Login to `git.dev.microservices.al` registry. + 10. Push image with `:main` and per-commit-SHA tags. +- `package.json` `scripts.test` placeholder (Phase 3 will replace; for now, `"test": "echo \"no tests yet\" && exit 0"` so CI can include the step without failing). +- `README.md` updated: "Building locally" + "CI" sections. + +## Specification + +### Dockerfile shape + +```dockerfile +# syntax=docker/dockerfile:1.6 + +# ───────── builder ───────── +FROM node:22-alpine AS builder +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /build + +COPY package.json pnpm-lock.yaml ./ +RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ + pnpm fetch + +COPY . . +RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile --offline + +RUN pnpm build # produces dist/ + +# ───────── runner ───────── +FROM nginx:1.27-alpine AS runner +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /build/dist /usr/share/nginx/html + +EXPOSE 80 + +# nginx default CMD is correct; don't override. +``` + +Mirror the multi-stage + cache-mount pattern from `trm/processor/Dockerfile` precisely. Differences are SPA-specific (no runtime Node, just nginx). + +### nginx.conf shape + +```nginx +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA routing fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Hashed assets β€” long cache + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + # index.html β€” never cache + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires off; + } + + # Runtime config β€” overridable via volume mount + location = /config.json { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + try_files $uri =404; + } + + # Compression + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_min_length 1024; +} +``` + +The `location = /config.json` placement allows mounting a different file at `/usr/share/nginx/html/config.json` in stage/prod via Docker volume β€” that's the override mechanism for runtime config. + +### Workflow path filter + +Mirror processor's filter structure. Critical paths to include: + +```yaml +on: + push: + branches: + - main + - 'phase-*-**' + paths: + - 'src/**' + - 'public/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'index.html' + - 'tsconfig*.json' + - 'vite.config.ts' + - 'tailwind.config.ts' + - 'eslint.config.js' + - '.prettierrc' + - 'Dockerfile' + - 'nginx.conf' + - '.dockerignore' + - '.gitea/workflows/build.yml' +``` + +Docs-only changes (`.planning/`, `README.md` alone) skip CI. + +### Image tagging + +Two tags per push to `main`: + +1. `:main` β€” moves with each `main` commit. Used by stage's `compose.yaml` for "always latest." +2. `:` β€” pinned reference. Production pins to a specific SHA to avoid surprise rollouts. + +This matches the processor's tagging scheme. `trm/deploy/compose.yaml` already documents the `*_TAG` env var pattern; Phase 1.10 adds `SPA_TAG`. + +### Why nginx not a Node static server + +A 5MB static bundle doesn't need a Node runtime. nginx is: + +- Faster (compiled C, not JS). +- Smaller image (~20MB vs ~150MB for node:22-alpine + serve). +- Battle-tested for static-serving + SPA-routing. + +Match the processor's "right tool for the job" discipline. + +### CI step ordering + +`typecheck` before `lint` before `format:check` before `build` is intentional β€” the cheapest checks first, so a broken typecheck fails the build before lint/format burn cycles. `build` is last because it's the most expensive and is what actually goes into the image. + +## Acceptance criteria + +- [ ] `docker build -t trm-spa-test .` succeeds locally. +- [ ] `docker run -p 8080:80 trm-spa-test` serves the SPA at `http://localhost:8080`; refreshing on `/login` (after 1.7) serves `index.html` (SPA fallback works). +- [ ] `pnpm format:check` script exists and is green. +- [ ] On push to `main` in Gitea, the workflow runs all gates and pushes the image to the registry. +- [ ] The image is < 50MB total. +- [ ] On push that _only_ touches `.planning/` or `README.md`, CI is skipped. +- [ ] Per-commit SHA tag is present in the registry alongside `:main`. + +## Risks / open questions + +- **Pinning pnpm version.** Use `corepack prepare pnpm@ --activate` with the version pinned to whatever `package.json` `packageManager` says, if set. Otherwise pin to the minor (`pnpm@9` or whatever the team is on) to avoid surprises. +- **Build-time env vars.** None should bleed in. Verify by `docker run trm-spa-test cat /usr/share/nginx/html/index.html` and grep for any hardcoded localhost URL β€” if found, that's a runtime-config violation to chase. +- **Caching the pnpm store across CI runs.** Gitea Actions runners differ; the `--mount=type=cache,id=pnpm-store` only helps during a single build. For cross-run caching, use Gitea Actions' `actions/cache@v4` step, keyed on `pnpm-lock.yaml` hash. Match what processor does β€” it likely already has this pattern. + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/10-deploy-compose-block.md b/.planning/phase-1-foundation/10-deploy-compose-block.md new file mode 100644 index 0000000..b8b1c39 --- /dev/null +++ b/.planning/phase-1-foundation/10-deploy-compose-block.md @@ -0,0 +1,148 @@ +# Task 1.10 β€” Compose service block in trm/deploy + +**Phase:** 1 β€” Foundation +**Status:** ⬜ Not started +**Depends on:** 1.9 (image must be publishable) +**Wiki refs:** `docs/wiki/entities/react-spa.md`; `trm/deploy/compose.yaml`; `trm/deploy/README.md` + +## Goal + +Wire the SPA into the platform stack: add a service block to `trm/deploy/compose.yaml`, document `SPA_TAG` in `.env.example`, update the deploy README's Currently / First-deploy / Network sections to reference it. After this task, redeploying the stack pulls the SPA image and serves it under the same origin as Directus, behind the reverse proxy. + +This task touches `trm/deploy`, not `trm/spa` β€” but it's a SPA Phase 1 deliverable because the SPA isn't operationally complete until it's wired into the stack. + +## Deliverables + +- `trm/deploy/compose.yaml` updated: + - New `spa` service block (full shape below). + - Internal-only (`expose: '80'`, no `ports:`) β€” same pattern as `directus`. The reverse proxy fronts it. + - Volume mount for the runtime-config override: `/usr/share/nginx/html/config.json` overridable from a host file. +- `trm/deploy/.env.example` updated: + - New `SPA_TAG=main` (default). + - Section header for SPA-specific config (currently just the tag). +- `trm/deploy/README.md` updated: + - "Services in the stack" section: move SPA from Planned to Currently. + - "Network model" section: add the SPA paragraph (internal-only, served by the reverse proxy). + - "First-deploy checklist" section: add a "Verify SPA loads" step (browse to public URL, expect login page). + - "Runtime config override" subsection: how the `config.json` volume mount works for setting per-environment URLs / Google Maps key. + +## Specification + +### Compose service block + +```yaml +spa: + image: git.dev.microservices.al/trm/spa:${SPA_TAG:-main} + expose: + - '80' + volumes: + # Override the baked-in dev config with the per-environment one. + # The host path is whatever the operator configures in Portainer or .env; + # default points at a sibling file in this repo. + - ${SPA_CONFIG_FILE:-./spa-config.json}:/usr/share/nginx/html/config.json:ro + restart: unless-stopped + networks: + - default + depends_on: + # SPA can boot independently of Directus / Processor β€” it's just static files. + # The reverse proxy is what wires them together; SPA loading without backends + # would just show a "Failed to load" error, which is the right UX. + [] +``` + +The `:ro` mount means the container can't accidentally write to its own config. Defensive. + +### Per-environment config file + +A sibling file `trm/deploy/spa-config.json` (NOT committed; in `.gitignore`) is created per environment. Operators copy from `spa-config.example.json` (committed) and edit: + +```json +{ + "directusUrl": "https://stage.trmtracking.org/api", + "liveWsUrl": "wss://stage.trmtracking.org/ws-live", + "businessWsUrl": "wss://stage.trmtracking.org/ws-business", + "env": "stage" +} +``` + +For stage with the proxy in place, the URLs are relative (just `/api`, `/ws-live`, etc.) β€” same pattern as the dev defaults. Absolute URLs are only needed if the SPA ever runs cross-origin to its backends, which it shouldn't. + +`spa-config.example.json` (committed): + +```json +{ + "directusUrl": "/api", + "liveWsUrl": "/ws-live", + "businessWsUrl": "/ws-business", + "env": "stage" +} +``` + +Operators copy β†’ edit `env` to `prod` for prod / add `googleMapsKey` / etc. + +### Reverse proxy routing + +The reverse proxy (Traefik / Caddy / nginx β€” operator's choice; not part of this stack) is responsible for: + +1. `/` β†’ `http://spa:80` (everything under root that isn't a more specific match). +2. `/api/*` β†’ `http://directus:8055/...` (REST + GraphQL). +3. `/ws-business` β†’ `ws://directus:8055/websocket` (Directus WS). +4. `/ws-live` β†’ `ws://processor:8081` (Processor WS β€” when Phase 1.5 lands). + +The proxy itself is documented in `trm/deploy/README.md` but not part of the compose stack β€” it's a sibling stack or a host-level service. Different operators will use different proxies; the README gives examples but doesn't prescribe. + +### `.env.example` addition + +```bash +# --------------------------------------------------------------------- +# spa +# --------------------------------------------------------------------- + +# Image tag to pull. `main` auto-tracks the latest commit on the main branch. +# In production, pin to a specific commit SHA for reproducibility. +# Example: SPA_TAG=ab12cd3 +SPA_TAG=main + +# Path on the host to the runtime config file mounted into the SPA container +# at /usr/share/nginx/html/config.json. Defaults to a sibling file in this repo; +# create it from spa-config.example.json before first deploy. +# SPA_CONFIG_FILE=/srv/trm/spa-config.json +``` + +### `trm/deploy/README.md` updates + +In "Services in the stack" (under Currently): add the SPA row, remove from Planned. + +In "Network model": add the SPA paragraph: + +> - **spa** β€” static bundle served by nginx. Internal-only on `:80`. The reverse proxy serves the SPA at `/` (default route). Same-origin with Directus and Processor's WS so cookie auth flows naturally to all three. + +In "First-deploy checklist", add to step 1 (generate secrets) a callout that no SPA secrets are needed; in step 5 (watch the first boot) add "the SPA container starts in seconds β€” no internal migrations to run"; add a step 8 "Verify SPA loads": browse to `https:///` β†’ expect to land on `/login`. + +Add a new "Runtime config override" subsection after "First-deploy checklist": + +> The SPA reads `/config.json` at boot for environment-specific URLs and optional API keys. The image bakes a default for dev; in stage/prod, override by mounting a custom file: +> +> 1. Copy `spa-config.example.json` to `spa-config.json` (or wherever `SPA_CONFIG_FILE` points). +> 2. Edit `env` (`stage` / `prod`) and any optional keys. +> 3. Redeploy the stack β€” no SPA rebuild needed. + +## Acceptance criteria + +- [ ] `compose.yaml` parses cleanly (`docker compose config` returns no errors). +- [ ] After Portainer redeploy with the new compose, `docker compose ps` shows the SPA container running. +- [ ] `curl -i http:///` returns the SPA's `index.html` (status 200, content-type text/html). +- [ ] Browsing the public URL in a browser shows the login page. +- [ ] `curl http:///config.json` returns the override config (NOT the baked-in dev defaults). +- [ ] After login + navigation to `/`, the home page renders. The end-to-end Phase 1 happy path works against a stage stack that also has `directus` running. +- [ ] Phase 1.5 of [[processor]] hasn't landed yet β†’ the `/ws-live` proxy route 502s, but the SPA's home page still loads (no live map UI to try-and-fail yet). + +## Risks / open questions + +- **Reverse-proxy choice not in scope.** The deploy README documents Traefik / Caddy / nginx as options; this task doesn't prescribe one. If the operator hasn't set up a proxy, this task's acceptance can't be verified end-to-end. Add a note in the deploy README's "First-deploy checklist" step pointing at the proxy-setup gap. +- **`spa-config.json` not in version control.** Each operator maintains theirs; it lives in their secret store (1Password, Vaultwarden, or Portainer's environment-files feature). Worth flagging in the README. +- **WebSocket sticky sessions.** Multi-replica SPA + multiple Processor instances in Phase 3 may need sticky sessions at the reverse proxy so a client's WS stays on the same Processor instance across reconnects. Out of scope for Phase 1 (single Processor, single SPA replica). + +## Done + +(Filled in when the task lands.) diff --git a/.planning/phase-1-foundation/README.md b/.planning/phase-1-foundation/README.md new file mode 100644 index 0000000..75dc3f0 --- /dev/null +++ b/.planning/phase-1-foundation/README.md @@ -0,0 +1,115 @@ +# Phase 1 β€” Foundation + +The deployable shell. Scaffold + stack + Directus cookie auth + protected routes + Gitea CI + compose deploy block. After Phase 1 the SPA is _infrastructurally complete_: an operator can hit the public URL, log in, see a placeholder home page, log out. Live map and operator features are layered on in subsequent phases. + +## Outcome statement + +When Phase 1 is done: + +- A user with a valid Directus credential can browse to `https://stage.trmtracking.org` (or whatever subdomain the proxy is configured for), be redirected to `/login`, submit credentials, get a session, and land on a placeholder home page. +- Same-domain reverse proxy serves the SPA bundle, Directus REST, Directus business-plane WS, and (when Phase 1.5 of [[processor]] lands) the live-position WS β€” all under one origin so the auth cookie flows naturally to all three. +- Refresh-on-401 is automatic via Directus's `/auth/refresh`. The access token never touches `localStorage`. +- Route guards check `user.role` in shape, even though everyone's effectively admin until [[directus]] Phase 4. +- Logout clears in-memory state + signs out via Directus + redirects to `/login`. +- `pnpm typecheck`, `pnpm lint`, `pnpm test` are green; Gitea CI builds and pushes a Docker image; Portainer redeploys the stack from `trm/deploy`'s `compose.yaml`. + +## Sequencing + +``` +1.1 Scaffold (done) + └─→ 1.2 Stack rounding-out + β”œβ”€β†’ 1.3 Vite dev proxy + β”‚ └─→ 1.4 Runtime config endpoint + β”‚ └─→ 1.5 Directus auth client + β”‚ β”œβ”€β†’ 1.6 Login page + β”‚ β”‚ └─→ 1.7 Routing skeleton ───→ 1.8 Logout flow + β”‚ β”‚ + β”‚ └─→ (1.7 also depends on 1.5) + β”‚ + └─→ 1.9 Gitea CI + Dockerfile ───→ 1.10 Deploy compose block +``` + +1.9 + 1.10 (deploy plumbing) can be developed in parallel with 1.4–1.8 (auth flow) once 1.3 lands. They merge cleanly because they touch different parts of the repo. + +## Files modified + +Phase 1 produces this layout in `spa/`: + +``` +spa/ +β”œβ”€β”€ .gitea/workflows/build.yml +β”œβ”€β”€ public/ +β”‚ β”œβ”€β”€ config.json # runtime config; replaced by proxy in stage/prod +β”‚ └── (favicon, etc.) +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ main.tsx # already exists +β”‚ β”œβ”€β”€ App.tsx # rewritten to host the router +β”‚ β”œβ”€β”€ routes/ # TanStack Router file-based routes +β”‚ β”‚ β”œβ”€β”€ __root.tsx +β”‚ β”‚ β”œβ”€β”€ index.tsx # placeholder home +β”‚ β”‚ β”œβ”€β”€ login.tsx +β”‚ β”‚ └── _authed/ # protected route group +β”‚ β”‚ └── route.tsx +β”‚ β”œβ”€β”€ auth/ +β”‚ β”‚ β”œβ”€β”€ client.ts # Directus SDK client + auth helpers +β”‚ β”‚ β”œβ”€β”€ store.ts # Zustand auth store (user, status) +β”‚ β”‚ └── guard.ts # protected-route guard hook +β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”œβ”€β”€ load.ts # fetch /config.json + zod-validate +β”‚ β”‚ β”œβ”€β”€ schema.ts # zod schema for runtime config +β”‚ β”‚ └── context.tsx # React context provider +β”‚ β”œβ”€β”€ ui/ +β”‚ β”‚ β”œβ”€β”€ primitives/ # shadcn/ui components live here +β”‚ β”‚ └── pages/ +β”‚ β”‚ β”œβ”€β”€ login.tsx +β”‚ β”‚ └── home.tsx +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ └── query-client.ts # TanStack Query singleton +β”‚ └── styles/ +β”‚ └── globals.css # Tailwind directives +β”œβ”€β”€ Dockerfile +β”œβ”€β”€ nginx.conf # static-serve config +β”œβ”€β”€ compose.dev.yaml # local dev with Directus + Processor stubs (optional) +β”œβ”€β”€ package.json # extended with the Phase 1 deps +β”œβ”€β”€ tailwind.config.ts +β”œβ”€β”€ postcss.config.js +β”œβ”€β”€ components.json # shadcn/ui config +β”œβ”€β”€ .prettierrc +β”œβ”€β”€ eslint.config.js # extended for Tailwind + Prettier integration +└── README.md +``` + +## Tech stack additions to the existing scaffold + +The Phase 1.1 scaffold already includes Vite 8, React 19, TypeScript 6, ESLint 10, and `@vitejs/plugin-react`. Phase 1 adds: + +- **`tailwindcss` + `@tailwindcss/vite`** β€” utility CSS. Tailwind 4 (the Vite-plugin-based generation, not PostCSS) for cleanliness. +- **`shadcn/ui`** β€” copy-in component primitives. Not a runtime dep; the `components.json` config + `npx shadcn add ...` populates `src/ui/primitives/`. +- **`@tanstack/react-router`** + **`@tanstack/router-plugin`** β€” file-based routing, type-safe. +- **`@tanstack/react-query`** β€” server state cache for Directus calls. +- **`zustand`** β€” high-frequency client state (auth store now; live-position store in Phase 2). +- **`@directus/sdk`** β€” typed Directus client. +- **`zod`** β€” runtime validation (config schema, form validation). +- **`react-hook-form`** + **`@hookform/resolvers`** β€” form library. +- **`prettier`** + **`eslint-config-prettier`** + **`eslint-plugin-prettier`** β€” formatting. + +Test infra (Vitest + React Testing Library) deferred to Phase 3 β€” Phase 1's tests are minimal and unit-testable with no DOM. + +## Non-negotiable design rules (phase-specific) + +These are in addition to the global rules in the ROADMAP. + +1. **No build-time secrets.** No env vars baked into the Vite build that change between environments. Everything env-specific is in `/config.json`. +2. **Auth state lives in Zustand, not React Context.** Context cascades re-renders on every change; Zustand selectors don't. The auth store is small but it's read in many places. +3. **Directus SDK over hand-rolled `fetch`.** Use `@directus/sdk`'s typed client for REST; only fall back to bare `fetch` for the runtime-config endpoint (which is served by the proxy, not Directus). +4. **One `QueryClient` instance.** Module-level singleton, imported wherever needed. No provider re-creating it on render. + +## Acceptance for the phase as a whole + +- [ ] All 9 task files (1.2–1.10) done. +- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm dev` clean and runnable locally. +- [ ] `pnpm build` produces a deployable static bundle in `dist/`. +- [ ] Gitea CI builds and pushes the Docker image; Portainer redeploy puts the SPA at the public URL. +- [ ] Manual smoke against stage: navigate to public URL β†’ redirected to `/login` β†’ submit valid Directus credentials β†’ see placeholder home page β†’ click logout β†’ back to `/login`. End-to-end happy path works. +- [ ] Wrong credentials show an error on the login form, not a stack trace. +- [ ] Refresh-on-401 works without re-prompting login (verifiable by waiting for the access token to expire while idle on the home page; SPA silently refreshes). diff --git a/.planning/phase-2-live-map/README.md b/.planning/phase-2-live-map/README.md new file mode 100644 index 0000000..eeeb128 --- /dev/null +++ b/.planning/phase-2-live-map/README.md @@ -0,0 +1,58 @@ +# Phase 2 β€” Live monitoring map + +**Status:** ⬜ Not started β€” depends on [[processor]] Phase 1.5 landing + +The dogfood-day deliverable. After Phase 2, an operator opens the SPA, picks the active event, and watches the field move on a real-time map. Inherits the architecture documented in `docs/wiki/concepts/maps-architecture.md` from `docs/wiki/sources/traccar-maps-architecture.md`, with the deliberate divergences (rAF coalescer, Zustand, longer trail default, racing sprite set, native PostGIS GeoJSON) baked in from day one. + +## Outcome statement + +When Phase 2 is done: + +- A `MapLibre GL JS` singleton renders inside a detached `
` mounted by ``. Sources and layers are added by side-effect-only `Map*` components. +- The user can switch basemap between Esri World Imagery (satellite), OpenTopoMap, OSM raster, and (if a Google Maps key is in runtime config) Google Satellite via the `maplibre-google-maps` adapter. +- Sprite registry is preloaded at app boot: `rally-car / quad / ssv / motorcycle / runner / hiker / default` Γ— `success / error / neutral / info`. +- The SPA opens a WebSocket to Processor (per `docs/wiki/synthesis/processor-ws-contract.md`), sends `subscribe` for the active event, receives a snapshot, then streams positions. +- An rAF-coalescer buffers incoming positions per-device; one `setData` call per frame regardless of message rate. +- `MapPositions` renders devices on two GeoJSON sources (clustered non-selected + unclustered selected). Cluster expansion and selection-on-click work. +- `MapTrails` renders a per-device bounded ring buffer of recent positions as a polyline. Default 200 points; configurable. +- An event picker (sidebar or top bar) lets the operator switch between events they have access to. +- Camera control trio (`MapCamera` / `MapDefaultCamera` / `MapSelectedDevice`) handles fit-on-load, follow-selected, and manual interaction. +- A connection-status indicator shows WS state (connected / reconnecting / offline) and last-message-received age per device (subtle UI; doesn't dominate). + +## Why this is a separate phase + +- **Foundation must be solid.** Auth, routing, deploy, runtime config β€” all must work before adding the live channel. Phase 1 ships an empty shell on stage; Phase 2 fills it. +- **Depends on the processor side.** The WS contract is locked, but the producing endpoint must exist before the SPA can connect to it. [[processor]] Phase 1.5 is the gating dependency. +- **Map architecture is non-trivial.** The singleton + side-effect-component + rAF coalescer + GeoJSON setData stack is a coherent pattern that works as a whole. Bundling it into Phase 1 would inflate Phase 1 dramatically. + +## Tasks (sketched, not detailed) + +These get full task files when Phase 2 starts. For now, this is the planned shape: + +| # | Task | Notes | +| --- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| 2.1 | MapLibre singleton + `` + `mapReady` gate | Module-level instance, detached `
`, listener Set for ready transitions | +| 2.2 | Tile-source switcher | Esri / OpenTopoMap / OSM / optional Google via `maplibre-google-maps`; reads from runtime config | +| 2.3 | Sprite preload | Pre-rasterised at `devicePixelRatio` Γ— 4 colour variants; re-`addImage`'d after every style swap | +| 2.4 | WS client + rAF coalescer + Zustand position store | The throughput-discipline core. Per-device latest-position map + per-device bounded trail ring buffer | +| 2.5 | `MapPositions` (clustered + selected sources) | Symbol layer + direction arrow + cluster bubble; selection on click | +| 2.6 | `MapTrails` (bounded ring buffer, polyline rendering) | Default 200 points/device; possibly speed-coloured per segment (post-decision) | +| 2.7 | Event picker + sidebar | Reads events from Directus REST via TanStack Query; subscribes / unsubscribes the WS on switch | +| 2.8 | Camera control trio | One-shot fit / initial framing / reactive follow | +| 2.9 | Connection-status + per-device last-seen indicators | Small chrome elements; non-dominant | + +## Architectural boundaries to maintain + +- `src/map/` is a self-contained module. Imports `@/auth` (for the WS cookie) and `@/config` (for the runtime config), nothing else. The map subsystem must be deletable as a unit if we ever need to reroute (we won't). +- `src/live/` houses the WS client, position store, and the coalescer. Decoupled from the map module so the map renders whatever's in the store, regardless of source. +- No domain logic. The map shows positions; it doesn't know about classes, entries, penalties, or stages. Phase 2.5+ is when domain awareness lands. + +## Open questions blocking task-level detail + +(These get answered when Phase 2 starts.) + +1. **Live trail colouring.** Flat colour by device or speed-coloured per segment (Traccar's replay style)? Speed-coloured live is novel and informative for race operators; needs a decision before 2.6. +2. **Event-picker placement.** Top bar (always visible) or sidebar (collapsible)? Depends on the rest of the operator chrome. +3. **Cluster parameters.** Traccar uses `clusterMaxZoom: 14, clusterRadius: 50`. Rally density (50–500 vehicles spread across a country-scale stage) may want different values. Defer until we see real positions on the map. +4. **Per-device sidebar list.** Out of scope for v1 of Phase 2 or in scope? Leaning out β€” the map is the focus; a list is supplementary. +5. **What happens when the user has access to multiple events.** Picker shows all? Only the active ones (between `starts_at` and `ends_at`)? Decide before 2.7. diff --git a/.planning/phase-3-dogfood-readiness/README.md b/.planning/phase-3-dogfood-readiness/README.md new file mode 100644 index 0000000..aa3b379 --- /dev/null +++ b/.planning/phase-3-dogfood-readiness/README.md @@ -0,0 +1,42 @@ +# Phase 3 β€” Dogfood readiness + +**Status:** ⬜ Not started + +The set of operational features that turn a working pilot into something safe to put in front of race operators on race day. None of these tasks change correctness; they change the experience when things don't go perfectly. + +## Outcome statement + +When Phase 3 is done: + +- **Error boundaries** wrap each major feature region (map, sidebar, header). A crash in the map widget doesn't blank the whole UI; the operator sees a "Map crashed; reload to retry" panel and can keep using the rest. +- **Connection-state UI** is unmissable but not noisy. WS reconnecting β†’ small banner. WS offline > 30s β†’ larger warning. Per-device "last seen N seconds ago" in the device sidebar. +- **Mobile-responsive baseline.** The SPA doesn't crash on a 375px-wide viewport. Map fills the screen; sidebar collapses to a sheet. Not a separate mobile app β€” the same app, usable in a marshal's pocket browser. +- **Per-device detail panel.** Click a device on the map β†’ side panel showing recent positions, current vehicle/crew, last-seen, manual notes the marshal has typed. Doesn't add data flows; consumes existing snapshots. +- **Operator-friendly empty / loading states.** No blank screens during the snapshot fetch; "No devices reporting yet" message when the event has subscribed but no positions arrived. +- **Vitest + React Testing Library set up.** Unit tests for the auth store, the rAF coalescer, the position-store reducer. Not a comprehensive suite β€” enough that future changes don't silently regress critical paths. + +## Why this is a separate phase + +Phase 2 produces a working live map. Phase 3 makes that map _fielded_ β€” usable when networks are flaky, browsers are exotic, screens are tiny, and operators are stressed. None of these tasks block the map from working in a controlled demo, but all of them affect whether the dogfood is genuinely useful versus a mostly-working tech demo. + +## Tasks (sketched, not detailed) + +| # | Task | Notes | +| --- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| 3.1 | Error boundaries + crash-recovery UI | One per major region (map, sidebar, header). Logs to console + optional remote-error endpoint | +| 3.2 | Connection-state UI | Banner / chip in the header; per-device last-seen in the sidebar | +| 3.3 | Mobile-responsive baseline | Tailwind breakpoints; map full-screen on mobile; sidebar β†’ sheet | +| 3.4 | Per-device detail panel | Click β†’ side panel; reads from Directus + position store | +| 3.5 | Empty / loading state polish | "Connecting…", "Waiting for first positions…", "No devices in this event yet" | +| 3.6 | Vitest + React Testing Library setup + targeted tests | Auth store, coalescer, position-store reducer. Not exhaustive | +| 3.7 | Production logging discipline | Console-error captures + optional remote-shipper for stage | +| 3.8 | Visual brand pass | Apply the actual TRM brand if one exists by then. Logo + colour palette | + +## Acceptance for the phase as a whole + +- [ ] All eight tasks (3.1–3.8) done. +- [ ] Pulling the network mid-session shows a clear "reconnecting" state; reconnecting works; the operator's selection / event-picker state survives the gap. +- [ ] Loading the SPA on an iPhone 12 (or equivalent) viewport doesn't break layout. +- [ ] Clicking a device opens the detail panel; the panel shows the device's current vehicle/crew (joined from Directus) and recent positions (from the store). +- [ ] Vitest runs in CI; existing tests are green; coverage is documented but not gated on a percentage threshold. +- [ ] The dogfood-day operator sign-off: a marshal can use the SPA on race day without us standing over them. (This is the real acceptance gate; the rest is observable proxy.) diff --git a/.planning/phase-4-future/README.md b/.planning/phase-4-future/README.md new file mode 100644 index 0000000..2d7edaf --- /dev/null +++ b/.planning/phase-4-future/README.md @@ -0,0 +1,29 @@ +# Phase 4 β€” Future / optional + +**Status:** ❄️ Not committed + +Ideas on radar that may or may not become real tasks. Captured here so they don't get forgotten and so we have a place to push scope creep that surfaces during Phases 1–3. + +None of these are committed. Move them out of Phase 4 and into a numbered phase only when there's a concrete reason to do them. + +## Candidates + +- **Geometry editor.** Full CRUD on `geofences` / `waypoints` / `speed_limit_zones` via `@mapbox/mapbox-gl-draw`. Drawing tools, vertex snapping, polygon validation, import from roadbook coordinates. **Depends on [[directus]] Phase 2 β€” those collections don't exist yet.** When directus Phase 2 lands, promote this to its own SPA phase. + +- **Replay mode.** Read historical positions for an event + time range, animate them on the map at chosen playback speed. Useful for: post-race review, debugging "where did we lose position from this device", validating a new geofence against past races. Needs a Processor-side replay endpoint (mentioned in `trm/processor/.planning/phase-4-future/README.md` as "replay tooling"). + +- **Heatmaps / density visualisation via deck.gl.** Layer on top of MapLibre showing position density over time. For a 24-hour rally with 500 racers, the line density on a single stage is a useful at-a-glance "where did people struggle / where was traffic" view. deck.gl integrates as a layer on top of the existing map; no architectural rework. + +- **i18n (Albanian).** The dogfood is closed and the operators speak English. Public-facing pages (eventually, when they exist) need Albanian. `react-i18next` is the boring answer; defer until there's a public surface. + +- **Dark mode.** shadcn/ui supports it natively via Tailwind class strategy. Trivial to add when someone wants it; not worth doing speculatively. + +- **End-to-end tests with Playwright.** Login β†’ subscribe β†’ see positions β†’ log out, run against a stack with mocked Processor positions. Worth doing when (a) Phase 1 is stable and (b) regressions have started costing real time. Until then, manual smoke + Vitest + the dogfood itself are sufficient. + +- **Per-event leaderboard / standings page.** Read derived results from [[processor]]'s Phase 2 timing tables (`stage_results`). Out of dogfood scope (manual results), but a real product feature for "after dogfood succeeds and we want to add real value." + +- **Spectator-facing public live map.** Same map widget, no auth, scoped to one public event. Different deployment shape (CDN-friendly, no cookies, possibly a separate origin). Requires [[directus]] Phase 4 (permissions) to be landed first so authorization makes sense. + +- **Notifications.** Browser push for "device went silent" or "speed-limit zone violation" alerts. Requires service worker + the Notifications API + a way for [[processor]] to push notification triggers. Material work; high operator value if we get there. + +- **Operator chat / coordination.** Race directors and marshals talking to each other inside the SPA during the event. Out of architectural scope (TRM is a tracking platform, not a comms platform), but easy to imagine an embedded Matrix or similar widget. Park here until someone makes the case. diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f644029 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +dist +dist-ssr +pnpm-lock.yaml +*.log +.git +public +src/routeTree.gen.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4c44657 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "semi": true, + "printWidth": 100, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/README.md b/README.md index 7dbf7eb..e49fdc8 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,52 @@ -# React + TypeScript + Vite +# trm/spa -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +End-user single-page app for the TRM platform: race directors, marshals, timekeepers, and (post-Phase 4 of `trm/directus`) public-facing participants. Talks to `trm/directus` for REST/GraphQL + business-plane WebSocket and to `trm/processor` for the live-position WebSocket firehose. -Currently, two official plugins are available: +Architectural anchors live in `trm/docs/wiki/`: -- [@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/) +- `wiki/entities/react-spa.md` β€” this service. +- `wiki/concepts/maps-architecture.md` β€” map subsystem (Phase 2). +- `wiki/synthesis/processor-ws-contract.md` β€” live-position wire spec (Phase 2). +- `wiki/synthesis/directus-schema-draft.md` β€” schema this consumes. -## React Compiler +Implementation planning lives in `.planning/` β€” see `.planning/ROADMAP.md`. -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). +## Stack -## Expanding the ESLint configuration +- **Vite 8 + React 19 + TypeScript 6** β€” SPA build, no SSR. +- **Tailwind CSS 4** via `@tailwindcss/vite`. +- **shadcn/ui** primitives (slate base, new-york style) β€” copy-in components owned by us, in `src/ui/primitives/`. +- **TanStack Router + Query** β€” file-based routes, server-state cache. +- **Zustand** β€” high-frequency client state (auth, live positions in Phase 2). +- **`@directus/sdk`** β€” typed Directus client (REST + WS). +- **`zod`** β€” runtime validation (config, forms, WS protocol). +- **`react-hook-form` + `@hookform/resolvers`** β€” forms. -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +## Common scripts -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +| Command | Purpose | +| -------------------- | --------------------------------------------- | +| `pnpm dev` | Start the Vite dev server on `:5173`. | +| `pnpm build` | Type-check + production build into `dist/`. | +| `pnpm preview` | Serve the built `dist/` for smoke testing. | +| `pnpm typecheck` | Type-check only (no build). | +| `pnpm lint` | ESLint over the repo. | +| `pnpm format` | Prettier auto-format. | +| `pnpm format:check` | Prettier check only (CI gate). | +| `pnpm test` | Test runner (Phase 3 wires Vitest). | - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +## Adding a shadcn primitive - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +```sh +pnpm dlx shadcn@latest add ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +Components land in `src/ui/primitives/` per `components.json`'s alias config. Edit them freely β€” they're our code from the moment they land. -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +## Local dev -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` +The Vite dev proxy (Phase 1.3) routes `/api`, `/ws-business`, `/ws-live` to the appropriate local services so the SPA dev experience matches the stage/prod reverse-proxy topology under one origin. Until 1.3 lands, the SPA runs standalone. + +## CI + +`.gitea/workflows/build.yml` (lands in Phase 1.9) runs the four gates β€” typecheck, lint, format:check, build β€” and publishes a Docker image on push to `main`. diff --git a/components.json b/components.json new file mode 100644 index 0000000..730d6f8 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/ui/primitives", + "utils": "@/lib/utils", + "ui": "@/ui/primitives", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/eslint.config.js b/eslint.config.js index ef614d2..664d7cb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,12 +1,13 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import prettierRecommended from 'eslint-plugin-prettier/recommended'; +import { defineConfig, globalIgnores } from 'eslint/config'; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(['dist', 'src/routeTree.gen.ts']), { files: ['**/*.{ts,tsx}'], extends: [ @@ -14,9 +15,18 @@ export default defineConfig([ tseslint.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, + prettierRecommended, ], languageOptions: { globals: globals.browser, }, }, -]) + { + // shadcn/ui primitives intentionally co-export helpers (variants, hooks) + // alongside components β€” disable the fast-refresh rule for these files only. + files: ['src/ui/primitives/**/*.{ts,tsx}'], + rules: { + 'react-refresh/only-export-components': 'off', + }, + }, +]); diff --git a/package.json b/package.json index 8e964e8..592425b 100644 --- a/package.json +++ b/package.json @@ -6,23 +6,47 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "preview": "vite preview", "lint": "eslint .", - "preview": "vite preview" + "typecheck": "tsc -b --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "echo \"no tests yet\" && exit 0" }, "dependencies": { + "@directus/sdk": "^21.3.0", + "@hookform/resolvers": "^5.2.2", + "@tanstack/react-query": "^5.100.8", + "@tanstack/react-router": "^1.169.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.14.0", + "radix-ui": "^1.4.3", "react": "^19.2.5", - "react-dom": "^19.2.5" + "react-dom": "^19.2.5", + "react-hook-form": "^7.75.0", + "tailwind-merge": "^3.5.0", + "zod": "^4.4.2", + "zustand": "^5.0.12" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.2.4", + "@tanstack/react-query-devtools": "^5.100.8", + "@tanstack/router-devtools": "^1.166.13", + "@tanstack/router-plugin": "^1.167.32", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.2.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", + "prettier": "^3.8.3", + "tailwindcss": "^4.2.4", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc08282..595a72c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,16 +8,64 @@ importers: .: dependencies: + '@directus/sdk': + specifier: ^21.3.0 + version: 21.3.0 + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.75.0(react@19.2.5)) + '@tanstack/react-query': + specifier: ^5.100.8 + version: 5.100.8(react@19.2.5) + '@tanstack/react-router': + specifier: ^1.169.1 + version: 1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^1.14.0 + version: 1.14.0(react@19.2.5) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: specifier: ^19.2.5 version: 19.2.5 react-dom: specifier: ^19.2.5 version: 19.2.5(react@19.2.5) + react-hook-form: + specifier: ^7.75.0 + version: 7.75.0(react@19.2.5) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + zod: + specifier: ^4.4.2 + version: 4.4.2 + zustand: + specifier: ^5.0.12 + version: 5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.3.0) + version: 10.0.1(eslint@10.3.0(jiti@2.6.1)) + '@tailwindcss/vite': + specifier: ^4.2.4 + version: 4.2.4(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)) + '@tanstack/react-query-devtools': + specifier: ^5.100.8 + version: 5.100.8(@tanstack/react-query@5.100.8(react@19.2.5))(react@19.2.5) + '@tanstack/router-devtools': + specifier: ^1.166.13 + version: 1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/router-plugin': + specifier: ^1.167.32 + version: 1.167.32(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)) '@types/node': specifier: ^24.12.2 version: 24.12.2 @@ -29,28 +77,40 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.10(@types/node@24.12.2)) + version: 6.0.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)) eslint: specifier: ^10.2.1 - version: 10.3.0 + version: 10.3.0(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.5.5 + version: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1))(prettier@3.8.3) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@10.3.0) + version: 7.1.1(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: ^0.5.2 - version: 0.5.2(eslint@10.3.0) + version: 0.5.2(eslint@10.3.0(jiti@2.6.1)) globals: specifier: ^17.5.0 version: 17.6.0 + prettier: + specifier: ^3.8.3 + version: 3.8.3 + tailwindcss: + specifier: ^4.2.4 + version: 4.2.4 typescript: specifier: ~6.0.2 version: 6.0.3 typescript-eslint: specifier: ^8.58.2 - version: 8.59.1(eslint@10.3.0)(typescript@6.0.3) + version: 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) vite: specifier: ^8.0.10 - version: 8.0.10(@types/node@24.12.2) + version: 8.0.10(@types/node@24.12.2)(jiti@2.6.1) packages: @@ -88,6 +148,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -109,6 +173,18 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -121,6 +197,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@directus/sdk@21.3.0': + resolution: {integrity: sha512-ozK2OmlaBiCuP5RuL046CaGGqEnlOdl4Rg690qaOgbC0C45XnDZVeBlb+NDLTUqEj88iSX8Cv74G3qh0L2i7Yw==} + engines: {node: '>=22'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -169,6 +249,26 @@ packages: resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -214,6 +314,700 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -315,6 +1109,214 @@ packages: '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tailwindcss/node@4.2.4': + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + + '@tailwindcss/oxide-android-arm64@4.2.4': + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.4': + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.4': + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.4': + resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/history@1.161.6': + resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} + engines: {node: '>=20.19'} + + '@tanstack/query-core@5.100.8': + resolution: {integrity: sha512-ceYwSFOqjPwET5TA6IOYxzxlGc0ekyH/gfOtWkP0PX43rzX9bxW48Iuw8KAduKCToi4rJAQ6nRy2kAe8gszdmg==} + + '@tanstack/query-devtools@5.100.8': + resolution: {integrity: sha512-29D6k564h7eudwNdfRcq6Je2VFUWGxHwADPg1xC2yHxrovYBwZiqzIv/DkPRsK/EMoOIPIvPq+IU0uCxiQXYPA==} + + '@tanstack/react-query-devtools@5.100.8': + resolution: {integrity: sha512-BKpysWo1u3kVMtv92XOv/Gu6eCbE/IxBLJPs0GG/qyySUQvZI2h7mqRwyf8Aa6WfUoX8Yf2AAh0uugQLAr8KtQ==} + peerDependencies: + '@tanstack/react-query': ^5.100.8 + react: ^18 || ^19 + + '@tanstack/react-query@5.100.8': + resolution: {integrity: sha512-iNNEekixXU5vtAGKKZX2lx3jTooG5yNY+kv0wSgEdEYG0Mj0JM5bcuQtC35ZAP3nDopT6jciUK3xeX65U7AnfA==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router-devtools@1.166.13': + resolution: {integrity: sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/react-router': ^1.168.15 + '@tanstack/router-core': ^1.168.11 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router@1.169.1': + resolution: {integrity: sha512-MBtQKSvac3OCcsSa6oBpDrrN90IV47I6Gtv05NxhbFVh+gVjtqvs6HSU4XM9+y5sHZPgS+35eArflX4vM8GEnQ==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + 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 + + '@tanstack/router-core@1.169.1': + resolution: {integrity: sha512-x+2gIGKTTE1qAn7tLieGfrB5ciOviDmmi2ox9fAWUubRV+yTU5ruGFXocoCIWF+lB+SOtnHjo2E9BLSWyYoEmA==} + engines: {node: '>=20.19'} + hasBin: true + + '@tanstack/router-devtools-core@1.167.3': + resolution: {integrity: sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/router-core': ^1.168.11 + csstype: ^3.0.10 + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-devtools@1.166.13': + resolution: {integrity: sha512-Qs8gkyI7m+eAxG3VcIOHuTSsUfA5ZxZcOa99ZyIIIJFxW6hy1k+m2s1J0ZYN1SNlip8P2ofd/MHiqmR1IWipMg==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/react-router': ^1.168.15 + csstype: ^3.0.10 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.166.39': + resolution: {integrity: sha512-j2OW/UvpjM/DT9tHVmuhWW1k6UOezTRrPqBPZFFmIth0fY7iTPqK+Erqpo8r5yGTRGCbMvOS4sL3H2MldnIZew==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.167.32': + resolution: {integrity: sha512-i9BA6GzUCoM20UYZ77orXzHwD5zM0OQTtLuPNbqTTSG38CvR6viRFP/d+QFo2aRNyCvun8PR7zSa49bslSggEQ==} + engines: {node: '>=20.19'} + hasBin: true + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.169.1 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.161.7': + resolution: {integrity: sha512-VkY0u7ax/GD0qU6ZLLnfPC+UMxVzxRbvZp4yV4iUSXjgJZ/siAT5/QlLm9FEDJ9QDoC0VD9W7f00tKKreUI7Ng==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + + '@tanstack/virtual-file-routes@1.161.7': + resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} + engines: {node: '>=20.19'} + hasBin: true + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -423,6 +1425,21 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -432,10 +1449,18 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -444,9 +1469,23 @@ packages: caniuse-lite@1.0.30001791: resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -470,9 +1509,20 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + electron-to-chromium@1.5.349: resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} + enhanced-resolve@5.21.0: + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} + engines: {node: '>=10.13.0'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -481,6 +1531,26 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-plugin-react-hooks@7.1.1: resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} engines: {node: '>=18'} @@ -537,6 +1607,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -556,6 +1629,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -576,6 +1653,14 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -584,6 +1669,14 @@ packages: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -602,6 +1695,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -610,9 +1707,21 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isbot@5.1.39: + resolution: {integrity: sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -723,6 +1832,14 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@1.14.0: + resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -741,6 +1858,10 @@ packages: node-releases@2.0.38: resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -761,9 +1882,16 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -776,19 +1904,81 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom@19.2.5: resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: react: ^19.2.5 + react-hook-form@7.75.0: + resolution: {integrity: sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react@19.2.5: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + rolldown@1.0.0-rc.17: resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -806,6 +1996,16 @@ packages: engines: {node: '>=10'} hasBin: true + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} + engines: {node: '>=10'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -818,10 +2018,28 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.4: + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -850,6 +2068,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -859,6 +2081,31 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite@8.0.10: resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -902,6 +2149,9 @@ packages: yaml: optional: true + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -924,9 +2174,30 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.2: resolution: {integrity: sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==} + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@babel/code-frame@7.29.0': @@ -991,6 +2262,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -1006,6 +2279,16 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -1029,6 +2312,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@directus/sdk@21.3.0': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1045,9 +2330,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@2.6.1))': dependencies: - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1068,9 +2353,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.3.0)': + '@eslint/js@10.0.1(eslint@10.3.0(jiti@2.6.1))': optionalDependencies: - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) '@eslint/object-schema@3.0.5': {} @@ -1079,6 +2364,28 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@floating-ui/utils@0.2.11': {} + + '@hookform/resolvers@5.2.2(react-hook-form@7.75.0(react@19.2.5))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.75.0(react@19.2.5) + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -1123,6 +2430,755 @@ snapshots: '@oxc-project/types@0.127.0': {} + '@pkgr/core@0.2.9': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true @@ -1176,6 +3232,200 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.7': {} + '@standard-schema/utils@0.3.0': {} + + '@tailwindcss/node@4.2.4': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.0 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.4 + + '@tailwindcss/oxide-android-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide@4.2.4': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-x64': 4.2.4 + '@tailwindcss/oxide-freebsd-x64': 4.2.4 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-x64-musl': 4.2.4 + '@tailwindcss/oxide-wasm32-wasi': 4.2.4 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + + '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1))': + dependencies: + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + tailwindcss: 4.2.4 + vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1) + + '@tanstack/history@1.161.6': {} + + '@tanstack/query-core@5.100.8': {} + + '@tanstack/query-devtools@5.100.8': {} + + '@tanstack/react-query-devtools@5.100.8(@tanstack/react-query@5.100.8(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/query-devtools': 5.100.8 + '@tanstack/react-query': 5.100.8(react@19.2.5) + react: 19.2.5 + + '@tanstack/react-query@5.100.8(react@19.2.5)': + dependencies: + '@tanstack/query-core': 5.100.8 + react: 19.2.5 + + '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/react-router': 1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/router-devtools-core': 1.167.3(@tanstack/router-core@1.169.1)(csstype@3.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@tanstack/router-core': 1.169.1 + transitivePeerDependencies: + - csstype + + '@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/history': 1.161.6 + '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/router-core': 1.169.1 + isbot: 5.1.39 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@tanstack/react-store@0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) + + '@tanstack/router-core@1.169.1': + dependencies: + '@tanstack/history': 1.161.6 + cookie-es: 3.1.1 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + + '@tanstack/router-devtools-core@1.167.3(@tanstack/router-core@1.169.1)(csstype@3.2.3)': + dependencies: + '@tanstack/router-core': 1.169.1 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + optionalDependencies: + csstype: 3.2.3 + + '@tanstack/router-devtools@1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/react-router': 1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-router-devtools': 1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + csstype: 3.2.3 + transitivePeerDependencies: + - '@tanstack/router-core' + + '@tanstack/router-generator@1.166.39': + dependencies: + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.169.1 + '@tanstack/router-utils': 1.161.7 + '@tanstack/virtual-file-routes': 1.161.7 + jiti: 2.6.1 + magic-string: 0.30.21 + prettier: 3.8.3 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.167.32(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.169.1 + '@tanstack/router-generator': 1.166.39 + '@tanstack/router-utils': 1.161.7 + '@tanstack/virtual-file-routes': 1.161.7 + chokidar: 3.6.0 + unplugin: 3.0.0 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.161.7': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + ansis: 4.2.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.16 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.9.3': {} + + '@tanstack/virtual-file-routes@1.161.7': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -1199,15 +3449,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.1(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.59.1 - '@typescript-eslint/type-utils': 8.59.1(eslint@10.3.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.1(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.1 - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -1215,14 +3465,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.1(eslint@10.3.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.59.1 '@typescript-eslint/types': 8.59.1 '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.1 debug: 4.4.3 - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -1245,13 +3495,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.59.1(eslint@10.3.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.59.1 '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.1(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -1274,13 +3524,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.1(eslint@10.3.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.59.1 '@typescript-eslint/types': 8.59.1 '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3) - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -1290,10 +3540,10 @@ snapshots: '@typescript-eslint/types': 8.59.1 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@24.12.2))': + '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.10(@types/node@24.12.2) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1) acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -1308,14 +3558,40 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + balanced-match@4.0.4: {} baseline-browser-mapping@2.10.25: {} + binary-extensions@2.3.0: {} + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.25 @@ -1326,8 +3602,28 @@ snapshots: caniuse-lite@1.0.30001791: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + convert-source-map@2.0.0: {} + cookie-es@3.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1344,26 +3640,48 @@ snapshots: detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} + + diff@8.0.4: {} + electron-to-chromium@1.5.349: {} + enhanced-resolve@5.21.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@7.1.1(eslint@10.3.0): + eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.6.1)): + dependencies: + eslint: 10.3.0(jiti@2.6.1) + + eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1))(prettier@3.8.3): + dependencies: + eslint: 10.3.0(jiti@2.6.1) + prettier: 3.8.3 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.3.0(jiti@2.6.1)) + + eslint-plugin-react-hooks@7.1.1(eslint@10.3.0(jiti@2.6.1)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.3 - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.4.2 zod-validation-error: 4.0.2(zod@4.4.2) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@10.3.0): + eslint-plugin-react-refresh@0.5.2(eslint@10.3.0(jiti@2.6.1)): dependencies: - eslint: 10.3.0 + eslint: 10.3.0(jiti@2.6.1) eslint-scope@9.1.2: dependencies: @@ -1376,9 +3694,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.3.0: + eslint@10.3.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.5.5 @@ -1408,6 +3726,8 @@ snapshots: minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -1431,6 +3751,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -1443,6 +3765,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -1460,12 +3786,24 @@ snapshots: gensync@1.0.0-beta.2: {} + get-nonce@1.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 globals@17.6.0: {} + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + + graceful-fs@4.2.11: {} + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -1478,14 +3816,24 @@ snapshots: imurmurhash@0.1.4: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-number@7.0.0: {} + + isbot@5.1.39: {} + isexe@2.0.0: {} + jiti@2.6.1: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -1564,6 +3912,14 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@1.14.0(react@19.2.5): + dependencies: + react: 19.2.5 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -1576,6 +3932,8 @@ snapshots: node-releases@2.0.38: {} + normalize-path@3.0.0: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -1597,8 +3955,12 @@ snapshots: path-key@3.1.1: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} postcss@8.5.13: @@ -1609,15 +3971,119 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.8.3: {} + punycode@2.3.1: {} + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + react-dom@19.2.5(react@19.2.5): dependencies: react: 19.2.5 scheduler: 0.27.0 + react-hook-form@7.75.0(react@19.2.5): + dependencies: + react: 19.2.5 + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + get-nonce: 1.0.1 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + react@19.2.5: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + rolldown@1.0.0-rc.17: dependencies: '@oxc-project/types': 0.127.0 @@ -1645,6 +4111,12 @@ snapshots: semver@7.7.4: {} + seroval-plugins@1.5.2(seroval@1.5.2): + dependencies: + seroval: 1.5.2 + + seroval@1.5.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1653,29 +4125,42 @@ snapshots: source-map-js@1.2.1: {} + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.4: {} + + tapable@2.3.3: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.59.1(eslint@10.3.0)(typescript@6.0.3): + typescript-eslint@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.59.1(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.1(eslint@10.3.0)(typescript@6.0.3) - eslint: 10.3.0 + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + eslint: 10.3.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -1684,6 +4169,12 @@ snapshots: undici-types@7.16.0: {} + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -1694,7 +4185,26 @@ snapshots: dependencies: punycode: 2.3.1 - vite@8.0.10(@types/node@24.12.2): + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sync-external-store@1.6.0(react@19.2.5): + dependencies: + react: 19.2.5 + + vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -1704,6 +4214,9 @@ snapshots: optionalDependencies: '@types/node': 24.12.2 fsevents: 2.3.3 + jiti: 2.6.1 + + webpack-virtual-modules@0.6.2: {} which@2.0.2: dependencies: @@ -1719,4 +4232,12 @@ snapshots: dependencies: zod: 4.4.2 + zod@3.25.76: {} + zod@4.4.2: {} + + zustand@5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) diff --git a/src/App.tsx b/src/App.tsx index 0d6f4c6..d099ea5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,17 @@ +import { Button } from '@/ui/primitives/button'; function App() { - return ( - <> - SPA - - ) +
+
+

TRM

+

+ Phase 1 scaffold. Tailwind + shadcn primitives wired. +

+ +
+
+ ); } -export default App +export default App; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..2819a83 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/main.tsx b/src/main.tsx index 4aff025..9211a97 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.tsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './styles/globals.css'; +import App from './App.tsx'; createRoot(document.getElementById('root')!).render( , -) +); diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..a97e8b6 --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,4 @@ +@import 'tailwindcss'; + +/* shadcn/ui design tokens β€” populated by `pnpm dlx shadcn@latest add` once base color is chosen. + Until then, only Tailwind's defaults are in scope. */ diff --git a/src/ui/primitives/alert.tsx b/src/ui/primitives/alert.tsx new file mode 100644 index 0000000..3613154 --- /dev/null +++ b/src/ui/primitives/alert.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const alertVariants = cva( + 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/ui/primitives/button.tsx b/src/ui/primitives/button.tsx new file mode 100644 index 0000000..aec5bdc --- /dev/null +++ b/src/ui/primitives/button.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Slot } from 'radix-ui'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Button({ + className, + variant = 'default', + size = 'default', + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot.Root : 'button'; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/src/ui/primitives/card.tsx b/src/ui/primitives/card.tsx new file mode 100644 index 0000000..d92803c --- /dev/null +++ b/src/ui/primitives/card.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/src/ui/primitives/form.tsx b/src/ui/primitives/form.tsx new file mode 100644 index 0000000..33bb873 --- /dev/null +++ b/src/ui/primitives/form.tsx @@ -0,0 +1,152 @@ +'use client'; + +import * as React from 'react'; +import type { Label as LabelPrimitive } from 'radix-ui'; +import { Slot } from 'radix-ui'; +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from 'react-hook-form'; + +import { cn } from '@/lib/utils'; +import { Label } from '@/ui/primitives/label'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext({} as FormFieldContextValue); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext({} as FormItemContextValue); + +function FormItem({ className, ...props }: React.ComponentProps<'div'>) { + const id = React.useId(); + + return ( + +
+ + ); +} + +function FormLabel({ className, ...props }: React.ComponentProps) { + const { error, formItemId } = useFormField(); + + return ( +