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 (
+
+
+
+
+ } />
+ } />
+
+
+
+ );
+}
\ 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.author}
+
+
+
+ ))}
+
+
+
+ search(useSearchStore.getState().query, page - 1)}
+ disabled={page <= 1 || loading}
+ className="rounded-lg border border-zinc-700 px-3 py-1.5 hover:bg-zinc-800 disabled:opacity-40"
+ >
+ Previous
+
+ Page {page}
+ search(useSearchStore.getState().query, page + 1)}
+ disabled={!hasMore || loading}
+ className="rounded-lg border border-zinc-700 px-3 py-1.5 hover:bg-zinc-800 disabled:opacity-40"
+ >
+ Next
+
+
+
+ );
+}
\ 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.
+ ) : (
+
+ {refs.map((ref) => (
+
+
+ {ref.kind}
+
+
+ {ref.url}
+
+
+ ))}
+
+ )}
+
+
+
+ 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.
+
+
+
+
+
+ {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