---
name: od-ui-dev
description: Use for ANY UI work in overdeck — building, editing, or reviewing components, pages, tables, chips, dialogs, styling, tokens, or the /design-system gallery. Triggers on edits to apps/web/src/**, packages/deck-ui/src/**. Enforces shared-components-only, tokens-only, honest-data, both themes, gallery registration, slopgate green.
---

# Overdeck UI Developer

audience: AI coding agents first (Claude subagents AND codex implementers — codex: this file
binds you too; AGENTS.md routes you here). Rules = enforced review criteria. Work violating
them gets rejected.

Overdeck = local ops deck (dark-first dashboard) over a federated collector. Every UI element
is a named shared component, tokenized, both themes, honest data. Zero exceptions.

## Ground truth

- **Primitive layer for a NEW page = Astryx** (`@astryxdesign/core`, `@astryxdesign/lab`,
  StyleX) — owner ruling 2026-08-14, registered in
  `docs/plans/2026-08-14-requests-board-astryx.md`. Use its exports FIRST for every
  generic control. NEVER build a deck-ui equivalent of a control Astryx already ships.
- **`@platform-modules/ui-primitives` is RETIRED for overdeck.** NEVER import it into a
  new page. Existing pages that already use it (`apps/web/src/components/ci/CiContent.tsx`
  and peers) keep it until ported; port a page only when the task already touches it.
- **Component registry = `packages/deck-ui/src/index.ts`** (the barrel) — overdeck-SPECIFIC
  composition only (domain components, chips, tiles built ON the platform primitives).
  Read it BEFORE authoring anything. Not re-inlined here — the barrel is the single
  source of truth for deck-ui.
- **Live gallery = `/design-system`** (`apps/web/src/pages/design-system.astro`). Every
  deck-ui component appears there with all variants/states. A test asserts barrel exports ⊆
  gallery registrations — adding a component without registering it fails the gate.
- **Tokens**: `packages/deck-ui/src/tokens.css` + `@platform-modules/ui-tokens` theme.
  Raw values allowed ONLY inside tokens files.

## Pre-flight — BEFORE writing code

1. Read the barrel. Component (or one composable to your need) exists? **Reuse it.**
2. Check `/design-system` gallery for its variants — match existing visual vocabulary.
3. Only then consider new code — see law 1.

## HARD RULES

### 1. Shared-component-only law

- All UI composes `@overdeck/deck-ui` exports. Page files (`apps/web/src/pages/*.astro`)
  are thin routing shells → components in `apps/web/src/components/**` → deck-ui primitives.
- NO ad-hoc structural markup in apps/web: no hand-rolled tables/grids (new page → the
  Astryx table; already-ported-later page → its existing `DataTable`), no status chips
  (use `StatusChip`), no sort headers (`DataTable` `withSorting`),
  no menus (`ActionsMenu`), no tooltips (`useDeckTooltip`), no toasts (`UndoToast`),
  no icon buttons (`IconButton`), no KPI/stat tiles (`KpiTile`/`ScoreCard`).
- **NEVER create a new primitive without explicit user approval.** Believe one is needed?
  STOP; report: (1) what you tried to compose from existing exports, (2) why it fails,
  (3) proposed name + props API. Wait. Do NOT invent `MiniTable`, `SmallChip`, forks, or
  near-duplicates of an existing export.
- Approved new primitive: lives in `packages/deck-ui/src/`, colocated `.test.tsx`, exported
  from the barrel, registered in `/design-system` — all in the same change.

### 2. Tokens-only law

- No hardcoded colors/radii/shadows/z-index/durations in components — `var(--mod-*)` /
  token classes only. New visual value → token in `tokens.css` FIRST, then consume it.
- Both themes ALWAYS: dark default + `:root[data-theme='light']` override for every new
  token. Verify components in both themes (gallery has the toggle).

### 3. Honest-data law

- NEVER fabricate numbers, timestamps, statuses, or placeholders presented as data.
  Absent value → omit or em dash; unknown source → labeled data-coverage/gap note naming
  the missing field. Fixtures are fine ONLY where clearly fixture-scoped (tests, gallery).
- Timestamps: relative via `formatRelativeTime` + absolute ISO in a `useDeckTooltip`
  tooltip. Durations: `formatDurationMs` / `LiveDuration` (ticking). Never invent a start
  time from an unrelated field.
- Status vocabulary: `StatusChip` + `planStatusCategory` are the ONLY status→color source.

### 4. Layout + a11y law

- Icon glyphs fixed 16px (`h-4 w-4`) inside fixed icon buttons (`h-8 w-8`, ≥44px hit
  target). Icons NEVER sized by their container.
- Interactive = `<button>`/`<a>` only; never `<div onClick>`. `:focus-visible` ring on
  every focusable. Menus/dialogs keyboard-complete (Enter/Esc/arrows, outside-click close).
- Sortable headers: `aria-sort` + ▲/▼ via `SortableHeaderCell` only.
- Panes scroll inside their own `overflow-*-auto` containers; body never scrolls
  horizontally; pages are viewport-fit.
- Numbers users compare: `tabular-nums`.

### 5. Gallery registration law

- Every deck-ui component renders in `/design-system` with all meaningful variants/states
  (incl. empty/error/edge: long titles, zero counts, missing timestamps).
- The barrel⊆gallery test is part of the gate — keep it green, never skip/baseline it.

## Verification (all green before done)

```bash
pnpm --filter @overdeck/deck-ui test        # component tests
pnpm --filter @overdeck/deck-ui typecheck
pnpm --filter web build && pnpm --filter web typecheck
```

- Slopgate green (`.slopgate/` rules enforce laws 1–2 mechanically). NEVER suppress or
  baseline a violation your own change introduced.
- Live/Playwright probes: the app streams SSE — `networkidle` NEVER fires. Use
  `waitUntil: 'domcontentloaded'` + `waitForSelector`.

## Forbidden patterns

```
// reject: hand-rolled grid template for tabular data (gridTemplateColumns / grid-cols-[...])
// reject: rounded-full border px-2 chip markup outside StatusChip.tsx
// reject: raw <table> instead of DataTable; DiffViewer.tsx alone may preserve paired code-line alignment
// reject: aria-sort markup outside SortableHeaderCell.tsx
// reject: hex/rgb color in a component file
// reject: duplicated status→class maps (planStatusCategory is the one source)
// reject: fabricated placeholder data rendered as live data
// reject: new deck-ui file without colocated test + barrel export + gallery registration
```

## Checklist per UI change

- [ ] Barrel read; reused existing exports where possible; zero unapproved new primitives
- [ ] Tokens only; both themes verified
- [ ] Honest data: absent → omitted/dash; timestamps rel+abs tooltip
- [ ] Gallery updated for any component change; barrel⊆gallery test green
- [ ] deck-ui tests + typecheck, web build + typecheck, slopgate — all green, zero new warnings

## Self-update — MANDATORY

This doc is the fleet's memory. You changed the architecture, wiring, commands, or
doctrine this skill describes — or a live incident just proved a rule here wrong or
missing → EDIT THIS FILE in the same landing as the change. Adopted AND rejected
decisions both go in, with the measured why. An owner reminder to record a lesson is
a failure of this rule, not the trigger for it.
