# Component Guide — Agent Instructions

Specifications for common UI components. Read when building, reviewing, or standardizing component design.

---

## Component Reuse & Organization

1. **Prefer existing components.** Before building anything new, check the project's components folder for an existing component that covers the use case — even partially. Extend or compose what exists rather than creating from scratch.
2. **Componentize for reuse.** If a piece of UI could feasibly be used in more than one place, extract it into its own file in the project's components folder (in an appropriate subdirectory), not inline in a page, layout, or section file.
3. **One component per file.** Each reusable component gets its own file. Co-locating multiple unrelated components in a single file makes them harder to find and import.

---

## Component Height Scale

Buttons and inputs MUST share the same heights. This is non-negotiable.

| Size | Height | Use |
|---|---|---|
| XS | 28px | Dense UIs, table actions, tags |
| SM | 32px | Secondary actions, compact forms |
| MD | 36–40px | Default for most interfaces |
| LG | 44–48px | Primary CTAs, mobile touch targets |
| XL | 56px | Hero CTAs, landing pages |

Horizontal padding on buttons = 2× vertical padding.

---

## Buttons

### States (ALL required)
- **Default:** base appearance
- **Hover:** primary and danger buttons **lighten** (primary: 600 → 500, danger: via `--error-hover`). Secondary and ghost **darken** one step. No scale transform on hover.
- **Active/Pressed:** `scale(0.97)` on mouse-down, `120ms ease-out`. This pressed shrink applies to **primary and danger** variants only. Secondary/ghost darken background on press but do not scale. Applies to single-step interactions only (click completes the action). Multi-step triggers (e.g., dropdown openers) do NOT scale — see philosophy.md Active / Pressed section.
- **Focus:** visible focus ring (2px offset, `--border-focus`)
- **Disabled:** reduced opacity (0.5), no pointer events
- **Loading:** spinner replaces label, same dimensions

### Hierarchy
1. **Primary:** solid fill (600 stop), high contrast — ONE per section
2. **Secondary:** neutral bordered — canvas bg, `--border-subtle` border, dark text
3. **Ghost:** transparent, no border, minimal background on hover
4. **Danger:** red variant of primary — `--red-600` fill, `--error-hover` on hover, `scale(0.97)` on press

See `philosophy.md` Color Application section for the full button color model.

### Icon Placement
- Leading (left): adds meaning ("+ New", "Search")
- Trailing (right): indicates behavior ("→", "↗", "▾")
- Icon-only: MUST have `aria-label` and tooltip on hover

---

## Inputs

### Anatomy
- Label above input (top-aligned = fastest completion)
- Label-to-input gap: `--space-1` (4px) to 6px
- Between form fields: `--space-4` to `--space-6` (MUST exceed label-to-input gap)
- Placeholder: format hint only, never the label
- Helper text: below input, `--fg-secondary` color

### States
- **Default:** subtle border (`--border-default`)
- **Hover:** border darkens slightly
- **Focus:** `--border-focus` border + ring (2px)
- **Filled:** same as default with value
- **Error:** `--error-default` border + icon + message replacing helper text
- **Disabled:** reduced opacity, no interaction

### Heights
Must match button heights in the same size class. A 40px button next to a 40px input must feel like one unit.

---

## Cards

### Rules
- Consistent padding across all cards in the same view (`--space-4` to `--space-6`)
- Gap between cards > padding inside cards
- Single clear purpose per card
- Actions at bottom or in header, never scattered
- Hover: background shift one surface level deeper; no shadow change
- Click target: entire card if it's navigational

### Anatomy
```
┌─────────────────────────┐
│  Image/Media (optional) │
├─────────────────────────┤
│  Eyebrow / Category     │  ← caption-01, --fg-secondary
│  Title                  │  ← header-02, font-weight 500
│  Description            │  ← body-02, --fg-secondary
│                         │
│  [Action]    [Action]   │  ← bottom-aligned
└─────────────────────────┘
```

---

## Tables

- Left-align text, right-align numbers
- Header row: sticky, slightly heavier weight or background (`--bg-surface-1`)
- Row height: 40–52px for comfortable scanning
- Hover state on rows for scannability (`--bg-surface-1`)
- Zebra striping OR borders, never both
- Sortable columns: clear directional arrow
- Empty state: centered message with action

---

## Navigation

### Top Nav
- Height: 48–64px
- Logo left, nav center or left, actions right
- Active state: bold weight + underline or background
- Mobile: collapse to hamburger at `--bp-md`

### Sidebar
- Width: 240–280px expanded, 64–72px collapsed
- Section headers for grouping
- Active state: background + left border or weight change
- Icons: 20–24px, consistent stroke weight
- Collapse trigger: clearly visible

### Tabs
- Bottom tabs on mobile: max 5 items
- Active tab: fill change + label weight
- Badge dots for notifications
- Swipeable content in mobile tab views

### Nested Hover

When an interactive element lives inside an already-hovered surface (e.g., a button inside a hovered nav footer, an action inside a hovered card), its hover state must **punch back to the parent's rest background** (`--hover-on-surface`), not step darker. This creates contrast by going lighter than the surrounding hover, rather than stacking darker on darker.

> **Note:** This rule is specifically about *nested* interactive elements inside already-hovered parents. It does not apply to the first-level hover on light/pastel surfaces — those follow the standard rule and darken one stop (e.g., `50 → 100`). See the Hover section in `philosophy.md`.

- Use `--hover-on-surface` as the hover background for nested interactive elements — this punches back toward the parent's rest background
- In nav contexts: parent is at `--nav-hover` (grey-200), child punches to `--nav-surface` (grey-100)
- On general surfaces: parent is at `--hover-surface` (grey-100), child punches to `--hover-on-surface` (grey-50)
- This rule applies to any nesting depth — never stack darker-on-darker for hover

---

## Modals

| Type | Max Width | Use |
|---|---|---|
| Confirmation | 400px | Delete, destructive actions |
| Form | 480px | Short forms, settings |
| Content | 640px | Articles, previews |
| Complex | 960px | Multi-step, dashboards |

### Rules
- Overlay: `var(--overlay)`
- Padding: `--space-6` to `--space-7`
- Close: top-right corner, always visible
- Actions: bottom-right, primary on the right
- Focus trap: Tab cycles within modal only
- Escape key closes
- Enter: 250–300ms ease-out, exit: 200ms ease-in
- Click backdrop to close (non-destructive modals only)

---

## Tooltips

- Max width: 240px
- Padding: `--space-2` `--space-3`
- Delay: 300–500ms before showing
- Position: auto-flip to stay in viewport
- Arrow pointing to trigger element
- Dark background (`--bg-inverse`), light text (`--fg-inverse`)

---

## Badges & Tags

- Height: 20–24px (inline with text)
- Padding: `--space-1` `--space-2`
- Border-radius: `--radius-full` (pill) or match component system
- Semantic colors: blue/info, green/success, yellow/warning, red/error
- Text: `label-01` token (12px, medium weight)
- Removable tags: include × icon with hover state

---

## Toast / Notifications

- Position: top-right or bottom-center
- Width: 320–400px
- Auto-dismiss: 5–8 seconds for info, never for errors
- Stack from newest on top
- Include close button
- Action link when relevant ("Undo", "View")
- Semantic border or icon (not just color)
