# Components — variant matrix & patterns

For copy-paste implementations, see `RECIPES.md`. This doc covers the *rules* behind each pattern.

---

## Interactive variant matrix

Every interactive control resolves five states from one of four variants. **Disabled never uses `opacity`** — it uses `bg-paper-2` + `text-ink-3`.

### Ghost — transparent default

For list rows, mode tabs, icon buttons, text-link CTAs. Disappears at rest.

| State | Background | Foreground | Border |
|---|---|---|---|
| default       | transparent  | `ink-2` | none |
| hover         | `paper-2`    | `ink`   | none |
| active        | `paper-3`    | `ink`   | none |
| focus-visible | (preserves)  | (preserves) | `.shadow-focus` (1px mint) |
| disabled      | transparent  | `ink-3` | none |

### Soft — paper-2 default

Secondary buttons, the workhorse. Surface plus hairline border.

| State | Background | Foreground | Border |
|---|---|---|---|
| default       | `paper-2` | `ink-2` | `hairline-2` |
| hover         | `paper-2` | `ink`   | `hairline-2` |
| active        | `paper-3` | `ink`   | `hairline-2` |
| focus-visible | (preserves) | (preserves) | `.shadow-focus` |
| disabled      | `paper-2` | `ink-3` | `hairline` |

### Filled-ink — primary CTA on paper

Send, save, the default destructive-confirm. The action that needs to read first on a paper surface.

| State | Background | Foreground | Border |
|---|---|---|---|
| default       | `ink`     | `paper` | none |
| hover         | `fg-1`    | `paper` | none |
| active        | `ink`     | `paper` | inset 1px paper 10% |
| focus-visible | (preserves) | (preserves) | `.shadow-focus` |
| disabled      | `paper-2` | `ink-3` | none |

### Filled-mint — accent action

Mark resolved, accept, ship. The "approval" / "this is the one" action. Use sparingly — at most one filled-mint per screen.

| State | Background | Foreground | Border |
|---|---|---|---|
| default       | `accent`   | `paper` | none |
| hover         | `mint-600` | `paper` | none |
| active        | `mint-700` | `paper` | none |
| focus-visible | (preserves) | (preserves) | `.shadow-focus` |
| disabled      | `paper-2`  | `ink-3` | none |

**This matrix applies to interactive controls only.** Static text uses the type-scale defaults from `docs/tokens.md`.

---

## Per-component patterns

### Button

Anatomy: `<button>` with the variant's class set + size class.
Sizes: `sm` (`h-row-dense` / 28px, `px-2`), `md` (`h-row` / 36px, `px-3`).
Density: no.
Keyboard: native button — Tab to focus, Enter/Space to activate.
ARIA: native. Add `aria-label` if icon-only.

Recipe: `RECIPES.md` §2.

### Field — input + textarea

Composer pattern.

- **Wrapper:** `bg-paper-2 rounded-md px-3 py-2 transition-default`
- **Idle:** wrapper bg `paper-2`, no border
- **Focus-within:** wrapper bg `paper`, `.shadow-focus` (1px mint ring)
- **Inner input:** `.no-focus-ring` to suppress the global `:focus-visible` ring (avoids the double-ring effect)
- **Placeholder:** `text-ink-3`
- **Error:** wrapper carries `0 0 0 1px var(--color-danger)` shadow

ARIA: caller provides `<label htmlFor>`. Helper text uses `.text-small text-ink-3`.

Recipe: `RECIPES.md` §1.

### Row — list row

Used for files, threads, menu items.

- Height: `h-row-loose` (44px) for files/threads, `h-row` (36px) for menu items
- Divider: `border-b border-hairline` — between rows, not around them
- Default: `text-ink-2`
- Hover: `bg-paper-2 text-ink`
- Active: `bg-paper-2 text-ink` + `box-shadow: inset 2px 0 0 var(--color-accent)` (the left rule)
- Focus-visible: `.shadow-focus`

ARIA: `role="button" tabIndex={0}`. Enter/Space activate.

Recipe: `RECIPES.md` §3.

### Card

- Surface: `bg-paper-2`
- Border: `border border-hairline` (always — cards earn a hairline because they're contained content)
- Radius: `rounded-md`
- Padding: `p-4` (16px)
- Shadow: none by default. Add `.shadow-1` only when the card is genuinely floating (drag preview, drop hint).

Recipe: `RECIPES.md` §4.

### Modal

- Surface: `bg-paper`, `rounded-lg`, `.shadow-3`
- Width: `max-w-chat` (720px) default
- Padding: `p-6` (24px)
- Backdrop: `.scrim` class (`rgba(14, 16, 20, 0.20)`), at `z-overlay`
- Modal body: at `z-modal`
- Title: `.text-h3` (22px / 500)
- ARIA: `role="dialog" aria-modal="true" aria-labelledby="modal-title"`
- Focus: traps inside; Esc closes; focus returns to trigger
- Cancel: `ghost` variant. Primary: `filled-ink` or `filled-mint`.

Recipe: `RECIPES.md` §5.

### Command palette

Same surface chrome as modal but at `z-cmdk` (above modals).

- Trigger: ⌘K
- Width: `max-w-chat` (720px)
- Surface: `bg-paper`, `rounded-lg`, `.shadow-3`
- Backdrop: `.scrim` at `z-overlay`
- Search input: same Field pattern, `paper-2` idle → `paper` + ring on focus
- Result rows: ghost variant, `h-row` height
- Selected row: `bg-paper-2` with mint left-rule
- Keyboard: ↑/↓ navigate, Enter activate, Esc dismiss

### Toolchip

Collapsible row used in chat for tool calls.

- Resting: full-width row, `bg-paper-2`, `h-row`, hairline border, `rounded-md`, hidden overflow
- Hover: `bg-paper-3`
- Tool name: `.text-mono` `text-ink-3`
- Summary: `.text-small text-ink-2 truncate`
- Expanded: input pane (`pre.text-mono text-ink-2`) + result pane (`.text-small text-ink`), separated by hairline
- Tool-only assistant turns suppress the `claude` eyebrow — handled in `index.css` via `:has(.aui-tool-chip:only-child)`

Recipe: `RECIPES.md` §8.

### Picker option list + recommendation callout

- Options: ghost-style rows, `h-row`, `rounded-sm`
- Recommended option: `bg-mint-100 text-ink`, label `text-mono text-mint-700`
- Only one recommendation per list

Recipe: `RECIPES.md` §9.

### Eyebrow + numbered section header

- Pattern: `<mono ordinal> · <eyebrow>` — `01 · activity`
- Ordinal: `.text-mono` (12px Geist Mono ink-3)
- Separator: `·` in `text-ink-3 .text-mono`
- Eyebrow: `.text-eyebrow` (11px / 500 / +0.16em UC)
- Lowercase the source string. The class applies `text-transform: uppercase`.

Recipe: `RECIPES.md` §7.

### Markdown surface — `.md`

Apply the `.md` class to any container rendering markdown.

Hierarchy from weight + space, never color:

- H1: 22px / 600 / -0.02em / 1.2
- H2: 18px / 600 / -0.015em / 1.25
- H3: 15px / 600 / -0.01em / 1.3
- Body: 15px / 400 / 1.65 / `text-ink`
- Lists: `<ul>` with 4px `ink-3` dot bullets, `<ol>` with mono numbered counters
- Inline code: `bg-paper-2`, `radius-xs`, mono 0.88em
- Block code: near-black bg (`#0E1014`), `paper` text, `rounded-md`
- Links: `text-ink` + 1px `hairline-2` underline → `text-accent` + `border-accent` on hover
- Blockquote: 2px `hairline-2` left rule, `text-ink-2`
- Table: `text-13`, header uppercase 11px `ink-2`, cell hairline borders

Recipe: `RECIPES.md` §6.

### Pill

- Height: `h-row-dense` (28px) — there is no separate pill height
- Radius: `rounded-pill`
- Padding: `px-2`
- Type: `.text-mono` (12px mono) for status chips, `.text-small` for labels
- Tones: pair `text-{ok,warn,danger}` or `text-accent` with `bg-paper-2`. There are no `*-soft` background tokens — the surface is always paper-2.

### Status dot

- 6×6px (`w-1.5 h-1.5` — within the allowed sub-grid)
- `rounded-full`
- Solid tone color: `bg-ok`, `bg-warn`, `bg-danger`, `bg-accent`
- Use inline next to a text label — never as the only state signal

### Toast

- Surface: `bg-paper`, `rounded-md`, `.shadow-2`, hairline border
- Position: `bottom-6 right-6`, at `z-toast`
- Width: 280–400px
- Tone: colored title (`text-ok` / `text-warn` / `text-danger` / `text-accent`) + matching icon
- ARIA: `role="alert" aria-live="assertive"` for danger; `role="status" aria-live="polite"` for the rest
- Auto-dismiss: 5s default; danger toasts persist until dismissed

Recipe: `RECIPES.md` §11.

### Empty state

Required on every list/panel that can be empty. Never blank.

- Container: dashed `hairline-2` border, `rounded-md`, `py-12 px-6`, centered
- Icon: 16×16 (`w-icon h-icon`), `text-ink-3`, stroke 1.5
- Title: `.text-h4`
- Body: `.text-body text-ink-2`, max-width 320px
- CTA: `soft` variant button (optional)

Recipe: `RECIPES.md` §10.

### Skeleton

- `bg-paper-2`, `rounded-sm`, `animate-skeleton-pulse` (defined in `index.css`)
- Use for loading lists, message placeholders, file previews
- ARIA: `aria-hidden="true"` — purely visual

Recipe: `RECIPES.md` §12.

### kbd

- Surface: `bg-paper`, inset hairline-2 box-shadow
- Type: 11px Geist sans, 500, `text-ink-2`
- Min-width 18px, height 18px, padding `0 5px`
- Radius: `rounded-xs`

```html
<kbd>⌘</kbd> <kbd>K</kbd>
```

### Icon system

- **Sizes:** 14, 16 (default), 20, 24px. No others.
- **Stroke width:** 1.5px (Lucide convention)
- **Color:** inherits `currentColor` from parent text
- **Touch target:** wrap icon-buttons in 32×32 hit area minimum

Default icon size class: `w-icon h-icon` (16px). For 20/24px override with `w-5 h-5` / `w-6 h-6`.

**Banned icons:** Sparkles, Wand, Brain, Bot. The product is the AI; these icons add noise. Hard rule.

### Streaming caret + typing dots

Defined in `index.css`. Use during chat streaming.

- `.cursor` — 2px wide, 1em tall, `bg-accent`, blink animation
- `.typing` — three 5px dots, `bg-ink-3`, sequenced bounce

```html
<span className="typing"><span className="d"/><span className="d"/><span className="d"/></span>
```

---

## Component contract

Every component file MUST start with this docstring (see `src/components/_TEMPLATE.tsx`):

```typescript
/**
 * Component: <Name>
 * @role     <UI role>
 * @variant  <ghost | soft | filled-ink | filled-mint — for interactive>
 * @states   <which apply>
 * @sizes    <which apply>
 * @density  <yes/no>
 * @keyboard <supported keys + behavior>
 * @aria     <role + ARIA attributes>
 * @tokens   <list of tokens consumed>
 */
```

PR review rejects components missing this header.
