diff --git a/README.md b/README.md index 99e5165..f6fbd53 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ analyze their contents. A lightweight, client-only pnpm monorepo. | Package | Role | Runtime | | ------------------- | ------------------------------------------------------ | ---------- | +| `apps/web` | React frontend: search + mod pages | Browser | | `apps/proxy` | Hono HTTP server: Workshop search + save fetch | Node | | `packages/tts` | Fetch save from Steam, BSON-parse to `TTSMod` | Node | | `packages/extract` | Analyze a `TTSMod`: objects, asset refs, downloads | Isomorphic | @@ -25,6 +26,10 @@ pnpm dev # runs the proxy at http://localhost:3000 Get a Steam Web API key at https://steamcommunity.com/dev/apikey (free). +To run the frontend alongside the proxy, open a second terminal and run +`pnpm --filter @tts/web dev` (serves at http://localhost:5173 and proxies +`/search`, `/items`, and `/health` to the backend). + ## API | Method | Path | Description | @@ -38,6 +43,7 @@ Get a Steam Web API key at https://steamcommunity.com/dev/apikey (free). ```sh pnpm dev # run the proxy (tsx watch) +pnpm dev:web # run the frontend (vite) pnpm build # compile all packages pnpm typecheck # typecheck all packages pnpm test # run the unit tests (vitest) diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..bdaa8fc --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + TTS Workshop + + +
+ + + \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..28b3ec9 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "@tts/web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.json && vite build", + "preview": "vite preview", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "lint": "echo \"no lint configured\"" + }, + "dependencies": { + "@tts/extract": "workspace:*", + "@tts/shared": "workspace:*", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", + "tailwindcss": "^4.3.3", + "typescript": "^5.7.2", + "vite": "^8.2.1", + "vitest": "^4.1.10" + } +} \ No newline at end of file diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..88a194f --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,28 @@ +import { Link, Route, Routes } from 'react-router-dom'; +import SearchPage from './pages/SearchPage'; +import ModPage from './pages/ModPage'; + +export default function App() { + return ( +
+
+
+ + TTS Workshop + + +
+
+
+ + } /> + } /> + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts new file mode 100644 index 0000000..7963c76 --- /dev/null +++ b/apps/web/src/api.ts @@ -0,0 +1,28 @@ +import type { SearchResult, TTSMod } from '@tts/shared'; + +const BASE = ''; + +async function getJson(path: string): Promise { + const res = await fetch(`${BASE}${path}`); + if (!res.ok) { + const body = (await res.json().catch(() => null)) as { error?: string } | null; + throw new Error(body?.error ?? `Request failed (${res.status})`); + } + return res.json() as Promise; +} + +/** Search the Workshop. */ +export function searchWorkshop(q: string, page = 1): Promise { + const params = new URLSearchParams({ q, page: String(page) }); + return getJson(`/search?${params.toString()}`); +} + +/** Fetch a full parsed TTS save. */ +export function fetchMod(id: string): Promise { + return getJson(`/items/${id}`); +} + +/** Build a URL for the raw save file download. */ +export function modFileUrl(id: string): string { + return `${BASE}/items/${id}/file`; +} \ No newline at end of file diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx new file mode 100644 index 0000000..e869db4 --- /dev/null +++ b/apps/web/src/components/SearchResults.tsx @@ -0,0 +1,57 @@ +import { Link } from 'react-router-dom'; +import { useSearchStore } from '../stores/searchStore'; + +export default function SearchResults() { + const { items, page, hasMore, loading, search } = useSearchStore(); + + if (loading) return null; + if (items.length === 0) { + return

No results yet.

; + } + + return ( +
+
    + {items.map((item) => ( +
  • + + {item.previewImageUrl && ( + {item.title} + )} +
    +

    {item.title}

    +

    {item.author}

    +
    + +
  • + ))} +
+ +
+ + Page {page} + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/index.css b/apps/web/src/index.css new file mode 100644 index 0000000..a461c50 --- /dev/null +++ b/apps/web/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..4681fc0 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + + + , +); \ No newline at end of file diff --git a/apps/web/src/pages/ModPage.tsx b/apps/web/src/pages/ModPage.tsx new file mode 100644 index 0000000..fc87b4d --- /dev/null +++ b/apps/web/src/pages/ModPage.tsx @@ -0,0 +1,76 @@ +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { collectRefs, flattenObjects } from '@tts/extract'; +import { useModStore } from '../stores/modStore'; +import { modFileUrl } from '../api'; + +export default function ModPage() { + const { id } = useParams<{ id: string }>(); + const { mod, loading, error, load } = useModStore(); + + useEffect(() => { + if (id) load(id); + }, [id, load]); + + if (loading) return

Loading mod…

; + if (error) return

{error}

; + if (!mod) return

No mod loaded.

; + + const objects = flattenObjects(mod); + const refs = collectRefs(mod); + + return ( +
+
+

Mod {id}

+

+ {mod.GameMode} · {mod.Date} · {objects.length} objects · {refs.length}{' '} + asset refs +

+ + Download save file + +
+ +
+

Asset references

+ {refs.length === 0 ? ( +

No external assets found.

+ ) : ( + + )} +
+ +
+

Objects

+
    + {objects.map((o) => ( +
  • + {o.Name} + {o.GUID} +
  • + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/SearchPage.tsx b/apps/web/src/pages/SearchPage.tsx new file mode 100644 index 0000000..d9a0281 --- /dev/null +++ b/apps/web/src/pages/SearchPage.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react'; +import { useSearchStore } from '../stores/searchStore'; +import SearchResults from '../components/SearchResults'; + +export default function SearchPage() { + const { query, search, loading, error } = useSearchStore(); + const [input, setInput] = useState(query); + + useEffect(() => { + if (query) search(query); + }, [query, search]); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + search(input); + }; + + return ( +
+
+

Search the Workshop

+

+ Find Tabletop Simulator Workshop items by name. +

+
+ +
+ setInput(e.target.value)} + placeholder="e.g. wingspan" + className="flex-1 rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm outline-none focus:border-zinc-500" + /> + +
+ + {error &&

{error}

} + {loading &&

Searching…

} + +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/stores/modStore.ts b/apps/web/src/stores/modStore.ts new file mode 100644 index 0000000..1fdabd2 --- /dev/null +++ b/apps/web/src/stores/modStore.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand'; +import type { TTSMod } from '@tts/shared'; +import { fetchMod } from '../api'; + +interface ModState { + id: string | null; + mod: TTSMod | null; + loading: boolean; + error: string | null; + load: (id: string) => Promise; + clear: () => void; +} + +export const useModStore = create((set) => ({ + id: null, + mod: null, + loading: false, + error: null, + load: async (id) => { + set({ loading: true, error: null, id }); + try { + const mod = await fetchMod(id); + set({ mod, loading: false }); + } catch (err) { + set({ loading: false, error: String(err) }); + } + }, + clear: () => set({ id: null, mod: null, error: null, loading: false }), +})); \ No newline at end of file diff --git a/apps/web/src/stores/searchStore.test.ts b/apps/web/src/stores/searchStore.test.ts new file mode 100644 index 0000000..be47744 --- /dev/null +++ b/apps/web/src/stores/searchStore.test.ts @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { SearchResult } from '@tts/shared'; +import { useSearchStore } from './searchStore'; + +const result: SearchResult = { + items: [ + { + id: '1', + title: 'Wingspan', + author: 'someone', + previewImageUrl: 'https://example.com/p.png', + }, + ], + page: 1, + hasMore: false, +}; + +afterEach(() => { + vi.unstubAllGlobals(); + useSearchStore.setState({ + query: '', + page: 1, + items: [], + hasMore: false, + loading: false, + error: null, + }); +}); + +describe('searchStore', () => { + it('stores results from a successful search', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(result)))); + await useSearchStore.getState().search('wingspan'); + const state = useSearchStore.getState(); + expect(state.items).toHaveLength(1); + expect(state.items[0]!.title).toBe('Wingspan'); + expect(state.loading).toBe(false); + expect(state.error).toBeNull(); + }); + + it('records an error on failure', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(new Response(JSON.stringify({ error: 'boom' }), { status: 502 })), + ); + await useSearchStore.getState().search('wingspan'); + const state = useSearchStore.getState(); + expect(state.error).toContain('boom'); + expect(state.items).toHaveLength(0); + }); + + it('ignores empty queries', async () => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + await useSearchStore.getState().search(' '); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/apps/web/src/stores/searchStore.ts b/apps/web/src/stores/searchStore.ts new file mode 100644 index 0000000..48816d0 --- /dev/null +++ b/apps/web/src/stores/searchStore.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand'; +import type { SearchResult, WorkshopItem } from '@tts/shared'; +import { searchWorkshop } from '../api'; + +interface SearchState { + query: string; + page: number; + items: WorkshopItem[]; + hasMore: boolean; + loading: boolean; + error: string | null; + search: (query: string, page?: number) => Promise; +} + +export const useSearchStore = create((set) => ({ + query: '', + page: 1, + items: [], + hasMore: false, + loading: false, + error: null, + search: async (query, page = 1) => { + if (!query.trim()) return; + set({ loading: true, error: null, query, page }); + try { + const result: SearchResult = await searchWorkshop(query, page); + set({ + items: result.items, + hasMore: result.hasMore, + loading: false, + }); + } catch (err) { + set({ loading: false, error: String(err) }); + } + }, +})); \ No newline at end of file diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..151aa68 --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..395902c --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src"] +} \ No newline at end of file diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..fe68fa3 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + // Proxy API calls to the Hono backend during development. + proxy: { + '/search': 'http://localhost:3000', + '/items': 'http://localhost:3000', + '/health': 'http://localhost:3000', + }, + }, +}); \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index 0fb9cab..b36df46 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,6 +27,7 @@ shared types/validation package. | Package | Role | Runtime | | ------------------ | ------------------------------------------- | ------------ | +| `apps/web` | React frontend: search + mod pages | Browser | | `apps/proxy` | Hono HTTP server: search + fetch endpoints | Node | | `packages/tts` | Fetch save from Steam, BSON-parse to `TTSMod` | Node | | `packages/extract` | Analyze a `TTSMod`: objects, refs, assets | Isomorphic | @@ -35,15 +36,19 @@ shared types/validation package. ## Dependency graph ``` -apps/proxy ──► packages/tts ──► packages/shared - │ │ - │ └──► (fetchMod → TTSMod) - ▼ -packages/extract ──► packages/tts (traverseMod) ──► packages/shared (types) +apps/web ──► apps/proxy ──► packages/tts ──► packages/shared + │ │ │ + │ │ └──► (fetchMod → TTSMod) + │ ▼ + └──► packages/extract ──► packages/shared (types) ``` ### Edges +- **`apps/web` → `apps/proxy`** — calls `/search`, `/items/:id`, and + `/items/:id/file` over HTTP. +- **`apps/web` → `packages/extract`** — uses `flattenObjects` / `collectRefs` + to analyze a loaded `TTSMod` in the browser. - **`apps/proxy` → `packages/tts`** — calls `fetchMod` / `getFileName` to serve item requests. - **`apps/proxy` → `packages/shared`** — uses shared types and zod schemas for @@ -90,11 +95,15 @@ packages/extract ──► packages/tts (traverseMod) ──► packages/shared - `typescript` (strict), `tsx` (dev runner), `eslint`, `prettier`. - `vitest` (unit tests), colocated as `*.test.ts` next to sources. +- `vite`, `@vitejs/plugin-react`, `tailwindcss`, `@tailwindcss/vite` — frontend + build/dev tooling. - `pnpm` workspaces for package management. ## Deployment / runtime shape - The proxy runs as a single Node process via `@hono/node-server`. +- `apps/web` is a static Vite build served separately; during development it + proxies `/search`, `/items`, and `/health` to the proxy. - `packages/extract` is published/consumed as a plain ESM module usable from a browser bundle or Node. - No shared state between requests; each request fetches fresh. \ No newline at end of file diff --git a/docs/decisions.md b/docs/decisions.md index 65d9188..256253b 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -95,4 +95,20 @@ available in Node. `` / `` elements. **Alternatives considered:** Raw `ArrayBuffer`. Rejected — less convenient for -frontend rendering. \ No newline at end of file +frontend rendering. + +## D9 — Frontend stack: React + React Router + Tailwind v4 + Zustand + +**Decision:** The frontend (`apps/web`) uses React with React Router for +routing, Tailwind CSS v4 for styling, and Zustand for state, built with Vite. + +**Context:** The frontend consumes the proxy API for search/fetch and +`packages/extract` directly for analysis. React is the de-facto standard for +this kind of tool; React Router provides declarative routes (`/` and +`/mod/:id`); Tailwind v4 is the current major version and integrates via +`@tailwindcss/vite`; Zustand is a minimal, unopinionated store that fits the +small amount of client state (search + mod). + +**Alternatives considered:** Next.js (heavier than needed for a client-only +tool); Redux Toolkit (more boilerplate than warranted); CSS Modules (no +utility styling). \ No newline at end of file diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 817d9a3..00235ad 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -21,20 +21,20 @@ Steam Workshop, fetching full TTS save files, and analyzing their contents. ## Non-goals (for now) - Backend traversal endpoints (deferred by design). -- Frontend app (later; will consume `packages/extract` directly). - Caching / Redis / multi-instance concerns. ## Architecture ``` -apps/proxy ──► packages/tts ──► packages/shared - │ │ - │ └──► (fetchMod → TTSMod) - ▼ -packages/extract ──► packages/tts (traverseMod) ──► packages/shared (types) - (isomorphic, used by future frontend) +apps/web ──► apps/proxy ──► packages/tts ──► packages/shared + │ │ │ + │ │ └──► (fetchMod → TTSMod) + │ ▼ + └──► packages/extract ──► packages/shared (types) + (isomorphic, used by the frontend) ``` +- **`apps/web`** — React frontend (search + mod pages). - **`apps/proxy`** — Hono server. Search + fetch only. No traversal endpoints. - **`packages/tts`** — low-level fetcher: Steam API call, BSON parse, filename derivation. Returns raw parsed `TTSMod`. @@ -54,16 +54,34 @@ tts-workshop/ ├── docs/ │ └── implementation-plan.md # this file ├── apps/ -│ └── proxy/ +│ ├── proxy/ +│ │ ├── package.json +│ │ ├── tsconfig.json +│ │ └── src/ +│ │ ├── index.ts # Hono app + @hono/node-server +│ │ ├── routes/ +│ │ │ ├── search.ts # GET /search?q=...&page=1 +│ │ │ ├── items.ts # GET /items/:id, /items/:id/file +│ │ │ └── health.ts # GET /health +│ │ └── env.ts # zod env validation +│ └── web/ │ ├── package.json │ ├── tsconfig.json +│ ├── vite.config.ts # dev proxy → localhost:3000 +│ ├── index.html │ └── src/ -│ ├── index.ts # Hono app + @hono/node-server -│ ├── routes/ -│ │ ├── search.ts # GET /search?q=...&page=1 -│ │ ├── items.ts # GET /items/:id, /items/:id/file -│ │ └── health.ts # GET /health -│ └── env.ts # zod env validation +│ ├── main.tsx # React root + router +│ ├── App.tsx # layout + routes +│ ├── api.ts # fetch wrappers for /search, /items +│ ├── index.css # tailwind v4 entry +│ ├── pages/ +│ │ ├── SearchPage.tsx +│ │ └── ModPage.tsx +│ ├── components/ +│ │ └── SearchResults.tsx +│ └── stores/ +│ ├── searchStore.ts # zustand +│ └── modStore.ts # zustand └── packages/ ├── shared/ │ └── src/ @@ -159,6 +177,27 @@ Hono server exposing search + fetch. - `GET /health` — liveness. - `env.ts` — zod validation of `STEAM_API_KEY`, `PORT`. +### `apps/web` + +React frontend (Vite + React Router + Tailwind v4 + Zustand). Consumes the +proxy API and `packages/extract` directly for analysis. + +- `main.tsx` — React root, `BrowserRouter`. +- `App.tsx` — app shell (header) + routes: `/` (search), `/mod/:id` (mod). +- `api.ts` — typed `fetch` wrappers for `/search` and `/items/:id`, plus a + `modFileUrl` helper for the raw-file download. +- `pages/SearchPage.tsx` — search form, drives `searchStore`. +- `pages/ModPage.tsx` — loads the mod via `modStore`, uses `@tts/extract` + (`flattenObjects`, `collectRefs`) to render objects and asset refs, and links + to the raw save file. +- `components/SearchResults.tsx` — result grid + pagination. +- `stores/searchStore.ts` / `stores/modStore.ts` — Zustand stores for search + and mod state. +- `vite.config.ts` — dev proxy for `/search`, `/items`, `/health` → + `http://localhost:3000`. +- Styling: Tailwind v4 via `@tailwindcss/vite`; `index.css` imports + `tailwindcss`. + ## Endpoints | Method | Path | Description | Auth | @@ -186,14 +225,16 @@ packages/extract → flatten objects / extract refs / download assets - `bson` — BSON deserialization. - `cheerio` — Workshop browse page scraping (backend only). - `zod` — validation. +- `react`, `react-dom`, `react-router-dom`, `zustand` — frontend. +- `vite`, `@vitejs/plugin-react`, `tailwindcss`, `@tailwindcss/vite` — frontend tooling. - `tsx`, `typescript`, `eslint`, `prettier` — tooling. ## Tooling - TypeScript strict mode. -- `tsx` for dev, `tsc` for build. +- `tsx` for dev, `tsc` for build; `vite` for the frontend dev server/build. - `vitest` for unit tests, colocated as `*.test.ts` next to sources. -- Root scripts: `pnpm dev`, `pnpm build`, `pnpm test`, `pnpm lint`. +- Root scripts: `pnpm dev`, `pnpm dev:web`, `pnpm build`, `pnpm test`, `pnpm lint`. ## Build order @@ -203,7 +244,9 @@ packages/extract → flatten objects / extract refs / download assets 3. `packages/tts` — fetch + parse (existing scraper code). 4. `packages/extract` — objects, refs, download. 5. `apps/proxy` — search + items + health routes, env validation. -6. Wire up root scripts, `.env.example`, README. +6. `apps/web` — React frontend (search + mod pages), consuming the proxy and + `packages/extract`. +7. Wire up root scripts, `.env.example`, README. ## Risks / caveats diff --git a/package.json b/package.json index 73fd934..2288f4b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "pnpm --filter @tts/proxy dev", + "dev:web": "pnpm --filter @tts/web dev", "build": "pnpm -r build", "lint": "pnpm -r lint", "typecheck": "pnpm -r typecheck", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd14a07..431c871 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.10 - version: 4.1.10(@types/node@22.20.1)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.11)) + version: 4.1.10(@types/node@22.20.1)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) apps/proxy: dependencies: @@ -43,6 +43,52 @@ importers: specifier: ^5.7.2 version: 5.9.3 + apps/web: + dependencies: + '@tts/extract': + specifier: workspace:* + version: link:../../packages/extract + '@tts/shared': + specifier: workspace:* + version: link:../../packages/shared + react: + specifier: ^19.2.8 + version: 19.2.8 + react-dom: + specifier: ^19.2.8 + version: 19.2.8(react@19.2.8) + react-router-dom: + specifier: ^7.18.2 + version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + zustand: + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.18)(react@19.2.8) + devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + '@types/react': + specifier: ^19.2.18 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.4 + version: 19.2.4(@types/react@19.2.18) + '@vitejs/plugin-react': + specifier: ^6.0.5 + version: 6.0.5(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vite: + specifier: ^8.2.1 + version: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@22.20.1)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + packages/extract: dependencies: '@tts/shared': @@ -240,9 +286,22 @@ packages: peerDependencies: hono: ^4 + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@oxc-project/types@0.143.0': resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} @@ -342,6 +401,100 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + 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.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -354,6 +507,27 @@ packages: '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + + '@vitejs/plugin-react@6.0.5': + resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + '@vitest/expect@4.1.10': resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} @@ -398,10 +572,21 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} @@ -431,40 +616,84 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hono@4.13.1: resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==} engines: {node: '>=16.9.0'} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-arm64@1.33.0: resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-darwin-x64@1.33.0: resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-freebsd-x64@1.33.0: resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} @@ -472,6 +701,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} @@ -479,6 +715,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} @@ -486,6 +729,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-musl@1.33.0: resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} @@ -493,18 +743,34 @@ packages: os: [linux] libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss-win32-x64-msvc@1.33.0: resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} @@ -535,11 +801,43 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-router-dom@7.18.2: + resolution: {integrity: sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.2: + resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + rolldown@1.2.3: resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -553,6 +851,13 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -673,6 +978,24 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + 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: '@esbuild/aix-ppc64@0.28.1': @@ -757,8 +1080,25 @@ snapshots: dependencies: hono: 4.13.1 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@oxc-project/types@0.143.0': {} '@rolldown/binding-android-arm64@1.2.3': @@ -807,6 +1147,74 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -820,6 +1228,19 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/react-dom@19.2.4(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 @@ -829,13 +1250,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.11))': + '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.11) + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) '@vitest/pretty-format@4.1.10': dependencies: @@ -869,8 +1290,17 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + + csstype@3.2.3: {} + detect-libc@2.1.2: {} + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + es-module-lexer@2.3.1: {} esbuild@0.28.1: @@ -915,41 +1345,94 @@ snapshots: fsevents@2.3.3: optional: true + graceful-fs@4.2.11: {} + hono@4.13.1: {} + jiti@2.7.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-android-arm64@1.33.0: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.33.0: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-darwin-x64@1.33.0: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.33.0: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.33.0: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.33.0: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.33.0: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.33.0: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.33.0: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.33.0: optional: true + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: dependencies: detect-libc: 2.1.2 @@ -986,6 +1469,27 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-router-dom@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + + react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie: 1.1.1 + react: 19.2.8 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + + react@19.2.8: {} + rolldown@1.2.3: dependencies: '@oxc-project/types': 0.143.0 @@ -1006,6 +1510,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.3 '@rolldown/binding-win32-x64-msvc': 1.2.3 + scheduler@0.27.0: {} + + set-cookie-parser@2.7.2: {} + siginfo@2.0.0: {} source-map-js@1.2.1: {} @@ -1014,6 +1522,10 @@ snapshots: std-env@4.2.0: {} + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + tinybench@2.9.0: {} tinyexec@1.3.0: {} @@ -1035,7 +1547,7 @@ snapshots: undici-types@6.21.0: {} - vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.11): + vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -1046,12 +1558,13 @@ snapshots: '@types/node': 22.20.1 esbuild: 0.28.1 fsevents: 2.3.3 + jiti: 2.7.0 tsx: 4.23.11 - vitest@4.1.10(@types/node@22.20.1)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.11)): + vitest@4.1.10(@types/node@22.20.1)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.11)) + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -1068,7 +1581,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.11) + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.1 @@ -1081,3 +1594,8 @@ snapshots: stackback: 0.0.2 zod@3.25.76: {} + + zustand@5.0.14(@types/react@19.2.18)(react@19.2.8): + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8