Files
llm-browser/frontend/AGENTS.md
T
2026-10-19 16:04:17 +02:00

126 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Northstar Design System — agent handbook
React 19 + Vite 8 + TypeScript (strict) design-system showcase. Dark-first, token-driven UI with a light theme.
## Commands
| Task | Command |
|---|---|
| Dev server | `npm run dev` (vite.config.ts pins `0.0.0.0:5173`) |
| Full gate: lint + token contract + tests + typecheck + build | `npm run check` — run before declaring any change done |
| Lint only | `npm run lint` (oxlint) |
| Tests | `npm run test` (vitest, jsdom; suites in `tests/`) |
TypeScript is **strict** (`strict` + `noUncheckedIndexedAccess` in `tsconfig.app.json`) — implicit `any` fails the build, not just the reviewer.
## Debugging without a console
Browser tooling here has no console access. Runtime errors therefore render into a `#dev-errors` element (see `src/dev/errorSink.ts` + `src/dev/ErrorBoundary.tsx`): after loading the page, `wait_for` / `inspect` `[data-dev-error]` to read error messages, and `wait_for vite-error-overlay` for build/HMR errors. If `#root` is empty but no `[data-dev-error]` exists, check the dev-server stdout.
## Sandboxed worker dev flow
- `vite.config.ts` allows `*.preview.k6n.net`; expose port **5173** via `repo_instance_expose` for a preview URL.
- Removing `allowedHosts` breaks preview ingress with a 403 ("host not allowed"). Don't.
- `/api` is proxied to the Go backend (target `API_URL`, default `http://localhost:8080`) and forwards `X-Forwarded-Host`/`X-Forwarded-Proto`, so OIDC login works unchanged on localhost and preview hosts. The auth module is `src/auth/` (`useSession` hook, Topbar `UserMenu`, overview `SessionCard`) — server side lives in the backend's `internal/auth` + `/api/auth/*` (see backend/AGENTS.md).
## Design tokens — the contract
1. **Single source of truth: `src/tokens.css`.** Every font size, color, spacing, radius, and shadow color comes from a token. The palette is defined in modern CSS Color 4 `lab(...)` color space:
- Dark theme (`:root:not([data-theme='light'])`, `[data-theme='dark']`, `.dark`): Pitch black base (`lab(0% 0 0)`), deep cards (`lab(.903296% 0 0)`), radiant electric mint/cyan primary (`lab(89.9321% -47.9163 -15.7303)`), vivid destructive (`lab(54.5811% 71.684 50.9279)`), chart colors 1–5, and subtle dot grid (`--dot-color`).
- Light theme (`:root`, `[data-theme='light']`, `.light`): Pure white daylight base (`lab(100% 0 0)`), crisp charcoal primary (`lab(7.78201% -.0000149012 0)`), and high-contrast surfaces.
- There are **no** legacy aliases (`--panel*`, `--line*`, `--text-soft`, …) anymore — canonical names only.
2. The guard `scripts/check-tokens.mjs` (in `npm run check`) bans hardcoded colors — hex, named colors, `rgb()/hsl()/oklch()/lab()`, even across multiple lines — plus px font sizes and `var(--x)` references that no CSS file defines. It scans **CSS files and inline `style={{ … }}` in `.ts/.tsx`**. Allowed: `var(--token)`, `color-mix(in srgb, var(--token) N%, transparent)`, `light-dark(var(--a), var(--b))`, and neutral black/white rgba shines.
3. Typography scale (`--text-3xs` … `--text-2xl`, `--text-hero`) is rem-based on a 9→28px grid. Longform body `--text-md`, buttons/nav `--text-xs`, meta/eyebrows `--text-2xs`/`--text-3xs`.
4. **Global density knob: `--root-font-size`** (default `112%` readability preset, baseline 16px = `100%`). One line scales every token-driven size app-wide; spacing tokens follow.
5. **Theming:** components read only `var(--*)` and never know the theme. A pre-paint script in `index.html` applies `localStorage['ns-theme']` or `prefers-color-scheme` before first paint (no flash); `useTheme` in `Topbar.tsx` reads what it set and keeps `data-theme` **and** the `.dark`/`.light` classes in sync on toggle.
## Smart Utils & AI Workflow
- **Global app state**: `src/app/` — `AppStateProvider` (mounted in `main.tsx`) owns the few settings the whole app shares: `theme`, `viewMode`, `sidebarCollapsed`, `searchOpen`, plus the ⌘K/Ctrl+K shortcut. Read them anywhere with `useAppState()` (import from `./app` or `../app`); it throws outside the provider on purpose. Add a setting to `appState.ts` (types) + `AppStateProvider.tsx` (state, DOM mirror, `STORAGE_KEYS` persistence) — never thread a new prop chain. Section-local state (active tab, selected pattern) stays in its section. Persisted settings are written **only on user action**, so an untouched app keeps following `prefers-color-scheme`. `theme` always boots from the `<html data-theme>` attribute the pre-paint script set (storage key `ns-theme` is shared with `index.html`); `viewMode` mirrors to `<html data-view-mode>` and currently gates nothing — it is the audience preset hook for per-persona views.
- **Class helper**: `src/utils/cn.ts` — zero-dependency classnames merging utility for conditional component styles.
- **Timer helper**: `src/utils/useTimeout.ts` — `schedule(fn, ms)` / `clear()`; auto-cancels pending timers on unmount. Never fire-and-forget a raw `setTimeout` in a component.
- **Smart CSS**: `src/utils.css` — ready-to-use helpers (`.dot-canvas`, `.surface-card`, `.surface-sidebar`, `.badge-*`, `.hover-lift`, `.kbd-tag`, `.skeleton-pulse`, `.progress-*`, `.ui-table-*`, `.dialog-*`, `.sheet-*`, `.alert-*`, `.ui-tabs-*`, `.switch-*`).
- **Overlays — one primitive**: `src/components/ui/Overlay.tsx` owns focus trap, Escape-to-close, scroll lock, focus restore, and conditional `aria-labelledby`/`aria-describedby` (wired only when a title/description is actually rendered). `Dialog` and `Sheet` are thin skins over it; `SearchPalette` reuses `trapTabKey` + `useOverlayLifecycle` from `src/components/ui/overlayBehavior.ts`. Build new modals on Overlay, not from scratch.
- **Composable Primitives**: `src/components/ui/` (`Overlay`, `Dialog`, `Sheet`, `Tabs`, `Alert`, `EmptyState`, `Table`, `Progress`, `Switch`, `Badge`, `Card`, `StatCard`, `Skeleton`, `Tooltip`, `Kbd`, `Avatar`, `Spinner`; `Icon` lives at `src/components/Icon.tsx` and is re-exported from the barrel).
- **StatCard**: `trend` is sentiment (badge color), `direction` is the arrow and defaults to the sign of `change` — a "good" latency drop is `trend="up"` with `change="-4.2ms"` rendering a green `↓ -4.2ms`.
- **Reusable Application Patterns**: `src/components/examples/` (`MetricsDashboard`, `SettingsPanel`, `ServiceTable`, `DeploymentModal`).
- **Icon Library**: the registry in `src/components/icon-registry.ts` maps every `IconName` to a lucide component (strictly typed — typos fail `tsc`, unknown names at runtime **throw in dev** so the error sink surfaces them). 42 curated icons power the showcase; `lucide-react` ships 1,400+ for direct import.
- **Routing — wouter (3 kB)**: the route table is `src/app/routes.tsx` and it drives the `<Switch>` in `App.tsx`, the sidebar, the breadcrumbs, the right rail and the ⌘K palette, so navigation cannot drift from the pages that exist. Adding a page = a `src/pages/*.tsx` component plus one registry entry (`path`, `group`, `label`, `index`, `kicker`, `title`, `description`, `meta`, `page`). Navigation is real links (`<Link href>`), never `onClick` + `scrollIntoView`; the shell scrolls to top on every location change. State that belongs in the URL lives in the query (`/patterns?pattern=table`), read with `useSearchParams` and validated by a registry helper (`patternFromSearch`). Deep links work because Vite serves the SPA fallback.
## Backend data — api / hooks / page separation
Live backend pages follow three layers and never skip one:
1. **`src/api/client.ts`** — the only `fetch` in the app. It knows the Go API's error envelope `{"error":{"code","message"}}` and turns every failure into an `ApiError` (`status`, `code`, `message`). Components switch on `code` (`validation_failed`, `not_found`, `network`, …), never on message strings. Override the origin with `VITE_API_URL`.
2. **`src/api/<resource>.ts`** — typed request functions per resource; `backend/internal/<resource>` is the source of truth for the wire format (camelCase JSON, bare arrays for lists).
3. **`src/hooks/use<Resource>.ts`** — SWR data hooks (`useItems`, `useItem`) plus one actions hook: mutations call the api module, then revalidate every matching cache entry through a key-filter `mutate`. All keys come from one factory (`itemsKeys`) so lists and details invalidate consistently. Lists use `keepPreviousData` so filter switches don't flash skeletons.
Pages import hooks only — never `src/api` directly, never `fetch`. The reference wiring is `src/pages/ItemsPage.tsx` at `/items`: skeleton rows on first load, a retry alert when the backend is down (with the exact `cd backend && make migrate-up && make run` command — SQLite is the backend default, no database server), empty state with a create CTA, per-row busy spinner, and inline action errors. Dev wiring: `vite.config.ts` proxies `/api` to the Go server on :8080 (override with `API_URL=… npm run dev`).
Adding a live resource: `src/api/<res>.ts` → `src/hooks/use<Res>.ts` → a page → route registry entry (+ `SearchPalette` SEARCH_INDEX entry) → tests mock `src/api/<res>` and assert the hook wiring (see `tests/useItems.test.tsx`).
## Structure
```
src/
tokens.css ← lab() design tokens + dark & light themes (edit this first)
utils.css ← token-compliant smart CSS utility classes
index.css ← fonts, tokens & utils import, base resets
App.css ← application component styles (token-driven, plain CSS)
App.tsx ← app shell: topbar + sidebar + breadcrumbs + <Switch> routes + right rail
main.tsx ← mounts App inside the ErrorBoundary, installs the dev error sink
app/
AppStateProvider.tsx ← global settings (theme, viewMode, sidebar, ⌘K search) + persistence
appState.ts ← context, types, useAppState()
routes.tsx ← route table: paths, headings, nav groups, URL helpers
persist.ts ← guarded localStorage read/write + storage keys
auth/ ← useSession() + UserMenu + SessionCard — OIDC identity via the /api proxy
api/ backend clients: client.ts (fetch wrapper + ApiError) and
one typed module per resource (items.ts)
hooks/ SWR data hooks + mutation actions per resource
(useItems.ts) — the only consumer of src/api
pages/ ← one component per route (OverviewPage, ButtonsPage, PatternsPage, …)
dev/
ErrorBoundary.tsx, errorSink.ts ← render runtime errors into #dev-errors
utils/
cn.ts ← conditional classnames utility
useTimeout.ts ← auto-cleaning setTimeout hook
components/
ui/ ← composable UI primitives (Overlay, Dialog, Sheet, Tabs, Alert,
EmptyState, Table, Progress, Switch, Badge, Card, StatCard,
Skeleton, Tooltip, Kbd, Avatar, Spinner) + overlayBehavior.ts
icon-registry.ts← the one icon name → lucide component map (single source)
examples/ ← reusable pattern components (MetricsDashboard, SettingsPanel,
ServiceTable, DeploymentModal)
AIWorkflowSection.tsx ← interactive pattern switcher & showcase
SearchPalette.tsx ← ⌘K command palette (Overlay behavior + combobox a11y)
TypographySection.tsx ← type-scale showcase
IconMatrix.tsx ← interactive Lucide icon explorer (renders iconRegistry)
Icon.tsx ← <Icon name="…" /> component (types from icon-registry.ts)
Topbar.tsx ← owns the theme toggle (data-theme + .dark/.light)
Hero.tsx, CodeSample.tsx, RightRail.tsx, ButtonMatrix.tsx,
Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx,
MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx
data/
library.ts ← demo content (button examples, color tokens)
patterns.ts ← pattern catalogue + PatternId (feeds /patterns?pattern=…)
tests/ ← vitest suites (jsdom): overlay, icon-registry, useTimeout,
app-state, routing, check-tokens guard
scripts/check-tokens.mjs ← token contract guard (part of npm run check)
```
## Parallel subagent workflow
Multiple agents can work the same checkout concurrently if tasks are **file-disjoint**:
- Each agent gets an explicit allow-list of files; shared files (barrels like `components/ui/index.ts`, `utils.css`, `App.tsx`) are edited by the orchestrator only, after agents finish.
- Agents verify with `npx oxlint <files>`, `npx tsc --noEmit -p tsconfig.app.json`, and `node scripts/check-tokens.mjs` — never `npm run check`/`vite build` in parallel (incremental build info and `dist/` would race).
- Read-only investigation tasks use the read-only spawn mode; they can run freely alongside writers.
- New primitives: agents create self-contained files (component + own `.css` import if keyframes/classes are needed); the orchestrator registers exports and showcase usage.
## Iteration workflow for visual changes
1. Change the token (or component markup) — never a literal.
2. `npm run check` until green.
3. Verify live: dev server + preview URL; screenshot at desktop width; toggle light/dark via the topbar button.