Files
gibbz/frontend/AGENTS.md
T
2026-09-22 08:41:20 +02:00

13 KiB
Raw Blame History

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.