# Cameleer3 Design System Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build a standalone React design system with ~35 components, theming, and 5 page layouts for the Cameleer3 operations dashboard.

**Architecture:** Three-tier component hierarchy (primitives → composites → layout) built on CSS Modules + CSS custom properties. All design tokens extracted from existing HTML mockups. Pages compose components with static mock data — no server needed.

**Tech Stack:** Vite, React 18, TypeScript, React Router v6, CSS Modules, Vitest + React Testing Library

**Spec:** `docs/superpowers/specs/2026-03-18-design-system-design.md`
**Mock reference:** `ui-mocks/mock-v2-light.html` (primary), `ui-mocks/mock-v3-*.html` (secondary pages)

---

## Phase 1: Scaffold & Foundations

### Task 1: Project Scaffold

**Files:**
- Create: `package.json`
- Create: `vite.config.ts`
- Create: `tsconfig.json`
- Create: `tsconfig.node.json`
- Create: `index.html`
- Create: `src/vite-env.d.ts`

- [ ] **Step 0: Ensure git is initialized**

The repo should already be initialized. If not:
```bash
cd C:/Users/Hendrik/Documents/projects/design-system
git init
echo "* text=auto eol=lf" > .gitattributes
```

- [ ] **Step 1: Initialize Vite project**

```bash
cd C:/Users/Hendrik/Documents/projects/design-system
npm create vite@latest . -- --template react-ts
```

Select "Ignore files and continue" if prompted (we have existing files).

- [ ] **Step 2: Install dependencies**

```bash
npm install react-router-dom
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event happy-dom
```

- [ ] **Step 3: Configure Vitest in vite.config.ts**

```ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  css: {
    modules: {
      localsConvention: 'camelCase',
    },
  },
  test: {
    globals: true,
    environment: 'happy-dom',
    setupFiles: ['./src/test-setup.ts'],
    css: { modules: { classNameStrategy: 'non-scoped' } },
  },
})
```

- [ ] **Step 4: Create test setup file**

Create `src/test-setup.ts`:
```ts
import '@testing-library/jest-dom/vitest'
```

- [ ] **Step 5: Update tsconfig.json**

Ensure `compilerOptions` includes:
```json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}
```

- [ ] **Step 6: Update index.html**

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=1920">
  <title>Cameleer3</title>
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>
</html>
```

- [ ] **Step 7: Verify build**

```bash
npm run dev
```

Expected: Vite dev server starts on localhost, page loads.

- [ ] **Step 8: Commit**

```bash
git add -A
git commit -m "scaffold: Vite + React + TypeScript + Vitest project setup"
```

---

### Task 2: Design Tokens & Reset CSS

**Files:**
- Create: `src/design-system/tokens.css`
- Create: `src/design-system/reset.css`
- Create: `src/index.css`

- [ ] **Step 1: Create tokens.css with all light theme values**

Create `src/design-system/tokens.css`:
```css
:root {
  /* Surface palette (warm parchment) */
  --bg-body:     #F5F2ED;
  --bg-surface:  #FFFFFF;
  --bg-raised:   #FAF8F5;
  --bg-inset:    #F0EDE8;
  --bg-hover:    #F5F0EA;

  /* Sidebar (warm charcoal) */
  --sidebar-bg:     #2C2520;
  --sidebar-hover:  #3A322C;
  --sidebar-active: #4A3F38;
  --sidebar-text:   #BFB5A8;
  --sidebar-muted:  #7A6F63;

  /* Text */
  --text-primary:   #1A1612;
  --text-secondary: #5C5347;
  --text-muted:     #9C9184;
  --text-faint:     #C4BAB0;

  /* Borders */
  --border:        #E4DFD8;
  --border-subtle: #EDE9E3;

  /* Brand accent (amber-gold) */
  --amber:       #C6820E;
  --amber-light: #F0D9A8;
  --amber-bg:    #FDF6E9;
  --amber-deep:  #8B5A06;

  /* Status colors (warm) */
  --success:        #3D7C47;
  --success-bg:     #EFF7F0;
  --success-border: #C2DFC6;
  --warning:        #C27516;
  --warning-bg:     #FEF5E7;
  --warning-border: #F0D9A8;
  --error:          #C0392B;
  --error-bg:       #FDF0EE;
  --error-border:   #F0C4BE;
  --running:        #1A7F8E;
  --running-bg:     #E8F5F7;
  --running-border: #B0DDE4;

  /* Typography */
  --font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;

  /* Spacing & Radii */
  --radius-sm: 5px;
  --radius-md: 8px;
  --radius-lg: 12px;

  /* Shadows */
  --shadow-sm:   0 1px 2px rgba(44, 37, 32, 0.06);
  --shadow-md:   0 2px 8px rgba(44, 37, 32, 0.08);
  --shadow-lg:   0 4px 16px rgba(44, 37, 32, 0.10);
  --shadow-card: 0 1px 3px rgba(44, 37, 32, 0.04), 0 0 0 1px rgba(44, 37, 32, 0.04);

  /* Chart palette */
  --chart-1: #C6820E;  /* amber */
  --chart-2: #3D7C47;  /* olive green */
  --chart-3: #1A7F8E;  /* teal */
  --chart-4: #C27516;  /* burnt orange */
  --chart-5: #8B5A06;  /* deep amber */
  --chart-6: #6B8E4E;  /* sage */
  --chart-7: #C0392B;  /* terracotta */
  --chart-8: #9C7A3C;  /* gold */
}

/* Dark theme — muted redesign, same token names */
[data-theme="dark"] {
  --bg-body:     #1A1714;
  --bg-surface:  #242019;
  --bg-raised:   #2A2620;
  --bg-inset:    #151310;
  --bg-hover:    #302B24;

  --sidebar-bg:     #141210;
  --sidebar-hover:  #1E1B17;
  --sidebar-active: #28241E;
  --sidebar-text:   #A89E92;
  --sidebar-muted:  #6A6058;

  --text-primary:   #E8E0D6;
  --text-secondary: #B0A698;
  --text-muted:     #7A7068;
  --text-faint:     #4A4238;

  --border:        #3A3530;
  --border-subtle: #2E2A25;

  --amber:       #D4941E;
  --amber-light: #4A3A1E;
  --amber-bg:    #2A2418;
  --amber-deep:  #E8B04A;

  --success:        #5DB866;
  --success-bg:     #1A2A1C;
  --success-border: #2A3E2C;
  --warning:        #D4901E;
  --warning-bg:     #2A2418;
  --warning-border: #3E3420;
  --error:          #E05A4C;
  --error-bg:       #2A1A18;
  --error-border:   #4A2A24;
  --running:        #2AA0B0;
  --running-bg:     #1A2628;
  --running-border: #243A3E;

  --shadow-sm:   0 1px 2px rgba(0, 0, 0, 0.2);
  --shadow-md:   0 2px 8px rgba(0, 0, 0, 0.3);
  --shadow-lg:   0 4px 16px rgba(0, 0, 0, 0.4);
  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.04);

  --chart-1: #D4941E;
  --chart-2: #5DB866;
  --chart-3: #2AA0B0;
  --chart-4: #D4901E;
  --chart-5: #E8B04A;
  --chart-6: #7AAE5E;
  --chart-7: #E05A4C;
  --chart-8: #B89A4C;
}
```

- [ ] **Step 2: Create reset.css**

Create `src/design-system/reset.css`:
```css
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { font-size: 14px; }

body {
  font-family: var(--font-body);
  background: var(--bg-body);
  color: var(--text-primary);
  line-height: 1.5;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }

/* Shared animations */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(61, 124, 71, 0.35); }
  50%      { box-shadow: 0 0 0 5px rgba(61, 124, 71, 0); }
}

@keyframes slideInRight {
  from { opacity: 0; transform: translateX(20px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
```

- [ ] **Step 3: Create index.css entrypoint**

Create `src/index.css`:
```css
@import './design-system/tokens.css';
@import './design-system/reset.css';
```

- [ ] **Step 4: Wire up main.tsx**

Create `src/main.tsx`:
```tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { ThemeProvider } from './design-system/providers/ThemeProvider'
import App from './App'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <ThemeProvider>
        <App />
      </ThemeProvider>
    </BrowserRouter>
  </StrictMode>,
)
```

- [ ] **Step 5: Create placeholder App.tsx**

Create `src/App.tsx`:
```tsx
export default function App() {
  return <div>Cameleer3 Design System</div>
}
```

- [ ] **Step 6: Commit**

```bash
git add src/design-system/tokens.css src/design-system/reset.css src/index.css src/main.tsx src/App.tsx
git commit -m "foundations: design tokens (light + dark) and CSS reset"
```

---

### Task 3: hashColor Utility

**Files:**
- Create: `src/design-system/utils/hashColor.ts`
- Create: `src/design-system/utils/hashColor.test.ts`

- [ ] **Step 1: Write failing tests**

Create `src/design-system/utils/hashColor.test.ts`:
```ts
import { describe, it, expect } from 'vitest'
import { hashColor, fnv1a } from './hashColor'

describe('fnv1a', () => {
  it('returns a number for any string', () => {
    expect(typeof fnv1a('test')).toBe('number')
  })

  it('returns consistent hash for same input', () => {
    expect(fnv1a('order-service')).toBe(fnv1a('order-service'))
  })

  it('returns different hashes for different inputs', () => {
    expect(fnv1a('order-service')).not.toBe(fnv1a('payment-service'))
  })
})

describe('hashColor', () => {
  it('returns bg, text, and border properties', () => {
    const result = hashColor('test')
    expect(result).toHaveProperty('bg')
    expect(result).toHaveProperty('text')
    expect(result).toHaveProperty('border')
  })

  it('returns HSL color strings', () => {
    const result = hashColor('order-service')
    expect(result.bg).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/)
    expect(result.text).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/)
    expect(result.border).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/)
  })

  it('returns consistent colors for same name', () => {
    const a = hashColor('VIEWER')
    const b = hashColor('VIEWER')
    expect(a).toEqual(b)
  })

  it('returns different hues for different names', () => {
    const a = hashColor('VIEWER')
    const b = hashColor('OPERATOR')
    expect(a.bg).not.toBe(b.bg)
  })

  it('accepts dark mode parameter', () => {
    const light = hashColor('test', 'light')
    const dark = hashColor('test', 'dark')
    expect(light.bg).not.toBe(dark.bg)
  })
})
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
npx vitest run src/design-system/utils/hashColor.test.ts
```

Expected: FAIL — module not found.

- [ ] **Step 3: Implement hashColor**

Create `src/design-system/utils/hashColor.ts`:
```ts
const FNV_OFFSET = 2166136261
const FNV_PRIME = 16777619

export function fnv1a(str: string): number {
  let hash = FNV_OFFSET
  for (let i = 0; i < str.length; i++) {
    hash ^= str.charCodeAt(i)
    hash = Math.imul(hash, FNV_PRIME)
  }
  return hash >>> 0
}

export function hashColor(
  name: string,
  theme: 'light' | 'dark' = 'light',
): { bg: string; text: string; border: string } {
  const hue = fnv1a(name) % 360

  if (theme === 'dark') {
    return {
      bg: `hsl(${hue}, 35%, 20%)`,
      text: `hsl(${hue}, 45%, 75%)`,
      border: `hsl(${hue}, 30%, 30%)`,
    }
  }

  return {
    bg: `hsl(${hue}, 45%, 92%)`,
    text: `hsl(${hue}, 55%, 35%)`,
    border: `hsl(${hue}, 35%, 82%)`,
  }
}
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
npx vitest run src/design-system/utils/hashColor.test.ts
```

Expected: All 7 tests PASS.

- [ ] **Step 5: Commit**

```bash
git add src/design-system/utils/
git commit -m "feat: hashColor utility with FNV-1a for deterministic badge/avatar colors"
```

---

### Task 4: ThemeProvider

**Files:**
- Create: `src/design-system/providers/ThemeProvider.tsx`
- Create: `src/design-system/providers/ThemeProvider.test.tsx`

- [ ] **Step 1: Write failing test**

Create `src/design-system/providers/ThemeProvider.test.tsx`:
```tsx
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ThemeProvider, useTheme } from './ThemeProvider'

function TestConsumer() {
  const { theme, toggleTheme } = useTheme()
  return (
    <div>
      <span data-testid="theme">{theme}</span>
      <button onClick={toggleTheme}>Toggle</button>
    </div>
  )
}

describe('ThemeProvider', () => {
  beforeEach(() => {
    localStorage.clear()
    document.documentElement.removeAttribute('data-theme')
  })

  it('defaults to light theme', () => {
    render(
      <ThemeProvider>
        <TestConsumer />
      </ThemeProvider>,
    )
    expect(screen.getByTestId('theme').textContent).toBe('light')
  })

  it('sets data-theme attribute on document', () => {
    render(
      <ThemeProvider>
        <TestConsumer />
      </ThemeProvider>,
    )
    expect(document.documentElement.dataset.theme).toBe('light')
  })

  it('toggles theme on button click', async () => {
    const user = userEvent.setup()
    render(
      <ThemeProvider>
        <TestConsumer />
      </ThemeProvider>,
    )
    await user.click(screen.getByText('Toggle'))
    expect(screen.getByTestId('theme').textContent).toBe('dark')
    expect(document.documentElement.dataset.theme).toBe('dark')
  })

  it('persists theme to localStorage', async () => {
    const user = userEvent.setup()
    render(
      <ThemeProvider>
        <TestConsumer />
      </ThemeProvider>,
    )
    await user.click(screen.getByText('Toggle'))
    expect(localStorage.getItem('cameleer-theme')).toBe('dark')
  })

  it('reads initial theme from localStorage', () => {
    localStorage.setItem('cameleer-theme', 'dark')
    render(
      <ThemeProvider>
        <TestConsumer />
      </ThemeProvider>,
    )
    expect(screen.getByTestId('theme').textContent).toBe('dark')
  })
})
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
npx vitest run src/design-system/providers/ThemeProvider.test.tsx
```

- [ ] **Step 3: Implement ThemeProvider**

Create `src/design-system/providers/ThemeProvider.tsx`:
```tsx
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'

type Theme = 'light' | 'dark'

interface ThemeContextValue {
  theme: Theme
  toggleTheme: () => void
  setTheme: (theme: Theme) => void
}

const ThemeContext = createContext<ThemeContextValue | null>(null)

const STORAGE_KEY = 'cameleer-theme'

function getInitialTheme(): Theme {
  try {
    const stored = localStorage.getItem(STORAGE_KEY)
    if (stored === 'dark' || stored === 'light') return stored
  } catch {
    // localStorage unavailable
  }
  return 'light'
}

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setThemeState] = useState<Theme>(getInitialTheme)

  const setTheme = useCallback((t: Theme) => {
    setThemeState(t)
    document.documentElement.dataset.theme = t
    try {
      localStorage.setItem(STORAGE_KEY, t)
    } catch {
      // localStorage unavailable
    }
  }, [])

  const toggleTheme = useCallback(() => {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }, [theme, setTheme])

  useEffect(() => {
    document.documentElement.dataset.theme = theme
  }, [theme])

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme(): ThemeContextValue {
  const ctx = useContext(ThemeContext)
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
  return ctx
}
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
npx vitest run src/design-system/providers/ThemeProvider.test.tsx
```

Expected: All 5 tests PASS.

- [ ] **Step 5: Commit**

```bash
git add src/design-system/providers/
git commit -m "feat: ThemeProvider with light/dark toggle and localStorage persistence"
```

---

## Phase 2: Primitives

Each primitive follows this pattern: `ComponentName.tsx` + `ComponentName.module.css` in `src/design-system/primitives/ComponentName/`. Each component gets a basic render test. Complex components get additional behavior tests.

**Testing approach for primitives:** Each component gets at minimum:
- Renders without error
- Key props produce expected output (class names, text content)
- Interactive components: click/change handlers fire

### Task 5: StatusDot + Spinner

**Files:**
- Create: `src/design-system/primitives/StatusDot/StatusDot.tsx`
- Create: `src/design-system/primitives/StatusDot/StatusDot.module.css`
- Create: `src/design-system/primitives/StatusDot/StatusDot.test.tsx`
- Create: `src/design-system/primitives/Spinner/Spinner.tsx`
- Create: `src/design-system/primitives/Spinner/Spinner.module.css`

- [ ] **Step 1: Write StatusDot test**

Create `src/design-system/primitives/StatusDot/StatusDot.test.tsx`:
```tsx
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { StatusDot } from './StatusDot'

describe('StatusDot', () => {
  it('renders a dot element', () => {
    const { container } = render(<StatusDot variant="success" />)
    expect(container.firstChild).toBeInTheDocument()
  })

  it('applies variant class', () => {
    const { container } = render(<StatusDot variant="error" />)
    expect(container.firstChild).toHaveClass('error')
  })

  it('applies pulse class for live variant by default', () => {
    const { container } = render(<StatusDot variant="live" />)
    expect(container.firstChild).toHaveClass('pulse')
  })

  it('disables pulse when pulse=false', () => {
    const { container } = render(<StatusDot variant="live" pulse={false} />)
    expect(container.firstChild).not.toHaveClass('pulse')
  })
})
```

- [ ] **Step 2: Implement StatusDot**

Create `src/design-system/primitives/StatusDot/StatusDot.tsx`:
```tsx
import styles from './StatusDot.module.css'

type StatusDotVariant = 'live' | 'stale' | 'dead' | 'success' | 'warning' | 'error' | 'running'

interface StatusDotProps {
  variant: StatusDotVariant
  pulse?: boolean
  className?: string
}

export function StatusDot({ variant, pulse, className }: StatusDotProps) {
  const showPulse = pulse ?? variant === 'live'
  const classes = [
    styles.dot,
    styles[variant],
    showPulse ? styles.pulse : '',
    className ?? '',
  ].filter(Boolean).join(' ')

  return <span className={classes} aria-hidden="true" />
}
```

Create `src/design-system/primitives/StatusDot/StatusDot.module.css`:
```css
.dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}

.live, .success { background: var(--success); }
.stale, .warning { background: var(--warning); }
.dead { background: var(--text-muted); }
.error { background: var(--error); }
.running { background: var(--running); }

.pulse { animation: pulse 2s ease-in-out infinite; }
```

- [ ] **Step 3: Implement Spinner**

Create `src/design-system/primitives/Spinner/Spinner.tsx`:
```tsx
import styles from './Spinner.module.css'

interface SpinnerProps {
  size?: 'sm' | 'md' | 'lg'
  className?: string
}

const sizes = { sm: 16, md: 24, lg: 32 }

export function Spinner({ size = 'md', className }: SpinnerProps) {
  const px = sizes[size]
  return (
    <span
      className={`${styles.spinner} ${className ?? ''}`}
      style={{ width: px, height: px }}
      role="status"
      aria-label="Loading"
    />
  )
}
```

Create `src/design-system/primitives/Spinner/Spinner.module.css`:
```css
.spinner {
  display: inline-block;
  border: 2px solid var(--border);
  border-top-color: var(--amber);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}
```

- [ ] **Step 4: Run tests**

```bash
npx vitest run src/design-system/primitives/StatusDot/
```

Expected: All 4 tests PASS.

- [ ] **Step 5: Commit**

```bash
git add src/design-system/primitives/StatusDot/ src/design-system/primitives/Spinner/
git commit -m "feat: StatusDot and Spinner primitives"
```

---

### Task 6: MonoText, KeyboardHint, SectionHeader

**Files:**
- Create: `src/design-system/primitives/MonoText/MonoText.tsx`
- Create: `src/design-system/primitives/MonoText/MonoText.module.css`
- Create: `src/design-system/primitives/KeyboardHint/KeyboardHint.tsx`
- Create: `src/design-system/primitives/KeyboardHint/KeyboardHint.module.css`
- Create: `src/design-system/primitives/SectionHeader/SectionHeader.tsx`
- Create: `src/design-system/primitives/SectionHeader/SectionHeader.module.css`

These are simple display components. Follow the same pattern as StatusDot.

- [ ] **Step 1: Implement MonoText**

```tsx
// MonoText.tsx
import styles from './MonoText.module.css'
import type { ReactNode } from 'react'

interface MonoTextProps {
  children: ReactNode
  size?: 'xs' | 'sm' | 'md'
  className?: string
}

export function MonoText({ children, size = 'md', className }: MonoTextProps) {
  return <span className={`${styles.mono} ${styles[size]} ${className ?? ''}`}>{children}</span>
}
```

```css
/* MonoText.module.css */
.mono { font-family: var(--font-mono); }
.xs { font-size: 10px; }
.sm { font-size: 11px; }
.md { font-size: 13px; }
```

- [ ] **Step 2: Implement KeyboardHint**

```tsx
// KeyboardHint.tsx
import styles from './KeyboardHint.module.css'

interface KeyboardHintProps {
  keys: string
  className?: string
}

export function KeyboardHint({ keys, className }: KeyboardHintProps) {
  return <kbd className={`${styles.kbd} ${className ?? ''}`}>{keys}</kbd>
}
```

```css
/* KeyboardHint.module.css */
.kbd {
  font-family: var(--font-mono);
  font-size: 10px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0 4px;
  color: var(--text-secondary);
  line-height: 1.6;
}
```

- [ ] **Step 3: Implement SectionHeader**

```tsx
// SectionHeader.tsx
import styles from './SectionHeader.module.css'
import type { ReactNode } from 'react'

interface SectionHeaderProps {
  children: ReactNode
  action?: ReactNode
  className?: string
}

export function SectionHeader({ children, action, className }: SectionHeaderProps) {
  return (
    <div className={`${styles.header} ${className ?? ''}`}>
      <span className={styles.label}>{children}</span>
      {action && <span className={styles.action}>{action}</span>}
    </div>
  )
}
```

```css
/* SectionHeader.module.css */
.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.label {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text-muted);
}

.action {
  font-size: 12px;
  color: var(--text-muted);
}
```

- [ ] **Step 4: Verify build compiles**

```bash
npx tsc --noEmit
```

- [ ] **Step 5: Commit**

```bash
git add src/design-system/primitives/MonoText/ src/design-system/primitives/KeyboardHint/ src/design-system/primitives/SectionHeader/
git commit -m "feat: MonoText, KeyboardHint, SectionHeader primitives"
```

---

### Task 7: Button + Input

**Files:**
- Create: `src/design-system/primitives/Button/Button.tsx`
- Create: `src/design-system/primitives/Button/Button.module.css`
- Create: `src/design-system/primitives/Button/Button.test.tsx`
- Create: `src/design-system/primitives/Input/Input.tsx`
- Create: `src/design-system/primitives/Input/Input.module.css`

- [ ] **Step 1: Write Button test**

```tsx
// Button.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'

describe('Button', () => {
  it('renders children text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument()
  })

  it('fires onClick', async () => {
    const onClick = vi.fn()
    const user = userEvent.setup()
    render(<Button onClick={onClick}>Click</Button>)
    await user.click(screen.getByRole('button'))
    expect(onClick).toHaveBeenCalledOnce()
  })

  it('shows spinner when loading', () => {
    render(<Button loading>Save</Button>)
    expect(screen.getByRole('status')).toBeInTheDocument()
  })

  it('is disabled when loading', () => {
    render(<Button loading>Save</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
})
```

- [ ] **Step 2: Implement Button**

```tsx
// Button.tsx
import styles from './Button.module.css'
import { Spinner } from '../Spinner/Spinner'
import type { ButtonHTMLAttributes, ReactNode } from 'react'

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
  size?: 'sm' | 'md'
  loading?: boolean
  children: ReactNode
}

export function Button({
  variant = 'secondary',
  size = 'md',
  loading = false,
  children,
  className,
  disabled,
  ...rest
}: ButtonProps) {
  return (
    <button
      className={`${styles.btn} ${styles[variant]} ${styles[size]} ${className ?? ''}`}
      disabled={disabled || loading}
      {...rest}
    >
      {loading && <Spinner size="sm" />}
      <span className={loading ? styles.hiddenText : ''}>{children}</span>
    </button>
  )
}
```

```css
/* Button.module.css */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-family: var(--font-body);
  font-weight: 500;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
}

.btn:disabled { opacity: 0.6; cursor: not-allowed; }

.sm { padding: 4px 10px; font-size: 11px; }
.md { padding: 6px 14px; font-size: 12px; }

.primary {
  background: var(--amber);
  color: white;
  border: none;
}
.primary:hover:not(:disabled) { background: var(--amber-deep); }

.secondary {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-secondary);
}
.secondary:hover:not(:disabled) { border-color: var(--text-faint); color: var(--text-primary); }

.danger {
  background: none;
  border: 1px solid var(--error-border);
  color: var(--error);
}
.danger:hover:not(:disabled) { background: var(--error-bg); }

.ghost {
  background: none;
  border: none;
  color: var(--text-secondary);
}
.ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }

.hiddenText { visibility: hidden; }
```

- [ ] **Step 3: Implement Input**

```tsx
// Input.tsx
import styles from './Input.module.css'
import { forwardRef, type InputHTMLAttributes, type ReactNode } from 'react'

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  icon?: ReactNode
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ icon, className, ...rest }, ref) => {
    return (
      <div className={`${styles.wrap} ${className ?? ''}`}>
        {icon && <span className={styles.icon}>{icon}</span>}
        <input
          ref={ref}
          className={`${styles.input} ${icon ? styles.hasIcon : ''}`}
          {...rest}
        />
      </div>
    )
  },
)

Input.displayName = 'Input'
```

```css
/* Input.module.css */
.wrap { position: relative; }

.icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-faint);
  font-size: 13px;
  pointer-events: none;
}

.input {
  width: 100%;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 12px;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.input::placeholder { color: var(--text-faint); }
.input:focus { border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-bg); }

.hasIcon { padding-left: 30px; }
```

- [ ] **Step 4: Run tests**

```bash
npx vitest run src/design-system/primitives/Button/
```

Expected: All 4 tests PASS.

- [ ] **Step 5: Commit**

```bash
git add src/design-system/primitives/Button/ src/design-system/primitives/Input/
git commit -m "feat: Button and Input primitives"
```

---

### Task 8: Select, Checkbox, Toggle

**Files:**
- Create: `src/design-system/primitives/Select/{Select.tsx,Select.module.css}`
- Create: `src/design-system/primitives/Checkbox/{Checkbox.tsx,Checkbox.module.css}`
- Create: `src/design-system/primitives/Toggle/{Toggle.tsx,Toggle.module.css}`

These are form primitives. Follow Input's styling patterns (warm focus ring, consistent border/padding).

- [ ] **Step 1: Implement Select** — styled `<select>` wrapping `options: { value: string; label: string }[]`, same focus ring as Input.

- [ ] **Step 2: Implement Checkbox** — styled `<input type="checkbox">` + `<label>`, amber checkmark, warm focus ring.

- [ ] **Step 3: Implement Toggle** — on/off switch using hidden checkbox + styled `<span>` track/thumb, amber active state.

- [ ] **Step 4: Verify build**

```bash
npx tsc --noEmit
```

- [ ] **Step 5: Commit**

```bash
git add src/design-system/primitives/Select/ src/design-system/primitives/Checkbox/ src/design-system/primitives/Toggle/
git commit -m "feat: Select, Checkbox, Toggle form primitives"
```

---

### Task 9: Badge, Avatar, Tag

**Files:**
- Create: `src/design-system/primitives/Badge/{Badge.tsx,Badge.module.css,Badge.test.tsx}`
- Create: `src/design-system/primitives/Avatar/{Avatar.tsx,Avatar.module.css,Avatar.test.tsx}`
- Create: `src/design-system/primitives/Tag/{Tag.tsx,Tag.module.css}`

These components use `hashColor`. Tests should verify color derivation.

- [ ] **Step 1: Write Badge test**

```tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Badge } from './Badge'

describe('Badge', () => {
  it('renders label text', () => {
    render(<Badge label="VIEWER" />)
    expect(screen.getByText('VIEWER')).toBeInTheDocument()
  })

  it('applies inline hash color when color is auto', () => {
    const { container } = render(<Badge label="VIEWER" color="auto" />)
    const badge = container.firstChild as HTMLElement
    expect(badge.style.backgroundColor).toBeTruthy()
  })

  it('applies semantic color class when specified', () => {
    const { container } = render(<Badge label="Failed" color="error" />)
    expect(container.firstChild).toHaveClass('error')
  })

  it('applies dashed variant class', () => {
    const { container } = render(<Badge label="inherited" variant="dashed" />)
    expect(container.firstChild).toHaveClass('dashed')
  })
})
```

- [ ] **Step 2: Implement Badge**

```tsx
// Badge.tsx
import styles from './Badge.module.css'
import { hashColor } from '../../utils/hashColor'
import { useTheme } from '../../providers/ThemeProvider'

type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'
type BadgeVariant = 'filled' | 'outlined' | 'dashed'

interface BadgeProps {
  label: string
  variant?: BadgeVariant
  color?: BadgeColor
  onRemove?: () => void
  className?: string
}

export function Badge({
  label,
  variant = 'filled',
  color = 'auto',
  onRemove,
  className,
}: BadgeProps) {
  const { theme } = useTheme()
  const isAuto = color === 'auto'
  const hashColors = isAuto ? hashColor(label, theme) : null

  const inlineStyle = isAuto
    ? {
        backgroundColor: variant === 'filled' ? hashColors!.bg : 'transparent',
        color: hashColors!.text,
        borderColor: hashColors!.border,
      }
    : undefined

  const classes = [
    styles.badge,
    styles[variant],
    !isAuto ? styles[color] : '',
    className ?? '',
  ].filter(Boolean).join(' ')

  return (
    <span className={classes} style={inlineStyle}>
      {label}
      {onRemove && (
        <button className={styles.remove} onClick={onRemove} aria-label={`Remove ${label}`}>
          &times;
        </button>
      )}
    </span>
  )
}
```

```css
/* Badge.module.css */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 10px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid transparent;
  text-transform: uppercase;
  letter-spacing: 0.3px;
}

.filled { /* default — inline styles from hashColor or semantic class */ }

.outlined {
  background: transparent !important;
}

.dashed {
  background: transparent !important;
  border-style: dashed;
}

.primary { background: var(--amber-bg); color: var(--amber-deep); border-color: var(--amber-light); }
.success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
.warning { background: var(--warning-bg); color: var(--warning); border-color: var(--warning-border); }
.error { background: var(--error-bg); color: var(--error); border-color: var(--error-border); }
.running { background: var(--running-bg); color: var(--running); border-color: var(--running-border); }

.remove {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  opacity: 0.5;
  padding: 0;
}
.remove:hover { opacity: 1; }
```

- [ ] **Step 3: Implement Avatar**

Avatar extracts initials from name, uses `hashColor` for background. Sizes: `sm` (24px), `md` (28px), `lg` (40px).

```tsx
// Avatar.tsx
import styles from './Avatar.module.css'
import { hashColor } from '../../utils/hashColor'
import { useTheme } from '../../providers/ThemeProvider'

interface AvatarProps {
  name: string
  size?: 'sm' | 'md' | 'lg'
  className?: string
}

function getInitials(name: string): string {
  const parts = name.split(/[\s-]+/).filter(Boolean)
  if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase()
  return name.slice(0, 2).toUpperCase()
}

export function Avatar({ name, size = 'md', className }: AvatarProps) {
  const { theme } = useTheme()
  const colors = hashColor(name, theme)
  return (
    <span
      className={`${styles.avatar} ${styles[size]} ${className ?? ''}`}
      style={{ backgroundColor: colors.bg, color: colors.text, borderColor: colors.border }}
      aria-label={name}
    >
      {getInitials(name)}
    </span>
  )
}
```

```css
/* Avatar.module.css */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-weight: 600;
  border: 1px solid;
  flex-shrink: 0;
}
.sm { width: 24px; height: 24px; font-size: 9px; }
.md { width: 28px; height: 28px; font-size: 11px; }
.lg { width: 40px; height: 40px; font-size: 14px; }
```

- [ ] **Step 4: Implement Tag**

Tag differs from Badge: it's always removable (primary use is active filter tags and group membership). Uses `hashColor` by default for color, or explicit semantic color. Has a prominent `x` dismiss button.

```tsx
// Tag.tsx
import styles from './Tag.module.css'
import { hashColor } from '../../utils/hashColor'
import { useTheme } from '../../providers/ThemeProvider'

type TagColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'

interface TagProps {
  label: string
  onRemove?: () => void
  color?: TagColor
  className?: string
}

export function Tag({ label, onRemove, color = 'auto', className }: TagProps) {
  const { theme } = useTheme()
  const isAuto = color === 'auto'
  const hashColors = isAuto ? hashColor(label, theme) : null

  const inlineStyle = isAuto
    ? { backgroundColor: hashColors!.bg, color: hashColors!.text, borderColor: hashColors!.border }
    : undefined

  const classes = [styles.tag, !isAuto ? styles[color] : '', className ?? ''].filter(Boolean).join(' ')

  return (
    <span className={classes} style={inlineStyle}>
      {label}
      {onRemove && (
        <button className={styles.remove} onClick={onRemove} aria-label={`Remove ${label}`}>
          &times;
        </button>
      )}
    </span>
  )
}
```

```css
/* Tag.module.css */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border: 1px solid;
  border-radius: 20px;
  font-size: 11px;
  font-family: var(--font-mono);
}

.primary { background: var(--amber-bg); color: var(--amber-deep); border-color: var(--amber-light); }
.success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
.warning { background: var(--warning-bg); color: var(--warning); border-color: var(--warning-border); }
.error { background: var(--error-bg); color: var(--error); border-color: var(--error-border); }
.running { background: var(--running-bg); color: var(--running); border-color: var(--running-border); }

.remove {
  cursor: pointer;
  opacity: 0.5;
  font-size: 13px;
  line-height: 1;
  background: none;
  border: none;
  color: inherit;
  padding: 0;
}
.remove:hover { opacity: 1; }
```

- [ ] **Step 5: Run tests**

```bash
npx vitest run src/design-system/primitives/Badge/ src/design-system/primitives/Avatar/
```

- [ ] **Step 6: Commit**

```bash
git add src/design-system/primitives/Badge/ src/design-system/primitives/Avatar/ src/design-system/primitives/Tag/
git commit -m "feat: Badge, Avatar, Tag primitives with hashColor integration"
```

---

### Task 10: Sparkline, Card, StatCard, FilterPill

**Files:**
- Create: `src/design-system/primitives/Sparkline/{Sparkline.tsx,Sparkline.test.tsx}`
- Create: `src/design-system/primitives/Card/{Card.tsx,Card.module.css}`
- Create: `src/design-system/primitives/StatCard/{StatCard.tsx,StatCard.module.css}`
- Create: `src/design-system/primitives/FilterPill/{FilterPill.tsx,FilterPill.module.css}`

- [ ] **Step 1: Implement and test Sparkline** (moved here from Task 12 because StatCard depends on it)

```tsx
// Sparkline.tsx
interface SparklineProps {
  data: number[]
  color?: string
  width?: number
  height?: number
  strokeWidth?: number
  className?: string
}

export function Sparkline({
  data,
  color = 'var(--amber)',
  width = 60,
  height = 20,
  strokeWidth = 1.5,
  className,
}: SparklineProps) {
  if (data.length < 2) return null

  const max = Math.max(...data)
  const min = Math.min(...data)
  const range = max - min || 1
  const padding = 1

  const points = data
    .map((val, i) => {
      const x = (i / (data.length - 1)) * (width - padding * 2) + padding
      const y = height - padding - ((val - min) / range) * (height - padding * 2)
      return `${x},${y}`
    })
    .join(' ')

  return (
    <svg
      width={width}
      height={height}
      viewBox={`0 0 ${width} ${height}`}
      className={className}
      aria-hidden="true"
    >
      <polyline
        points={points}
        fill="none"
        stroke={color}
        strokeWidth={strokeWidth}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  )
}
```

Test:
```tsx
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { Sparkline } from './Sparkline'

describe('Sparkline', () => {
  it('renders an SVG with a polyline', () => {
    const { container } = render(<Sparkline data={[1, 3, 2, 5, 4]} />)
    expect(container.querySelector('svg')).toBeInTheDocument()
    expect(container.querySelector('polyline')).toBeInTheDocument()
  })

  it('returns null for less than 2 data points', () => {
    const { container } = render(<Sparkline data={[1]} />)
    expect(container.firstChild).toBeNull()
  })
})
```

- [ ] **Step 2: Implement Card** — surface container with optional top accent stripe. Reference `ui-mocks/mock-v2-light.html` lines 527-553 for exact styles.

- [ ] **Step 3: Implement StatCard** — extends Card with: accent stripe, `.stat-label`, large `.stat-value` (mono), trend arrow, detail line. Accepts optional `sparkline` data (renders Sparkline inline). Reference mock lines 558-612.

- [ ] **Step 3: Implement FilterPill** — selectable pill with dot + count + active states. Reference mock lines 656-697.

- [ ] **Step 4: Verify build**

```bash
npx tsc --noEmit
```

- [ ] **Step 5: Commit**

```bash
git add src/design-system/primitives/Sparkline/ src/design-system/primitives/Card/ src/design-system/primitives/StatCard/ src/design-system/primitives/FilterPill/
git commit -m "feat: Sparkline, Card, StatCard, FilterPill primitives"
```

---

### Task 11: InfoCallout, EmptyState, CodeBlock

**Files:**
- Create: `src/design-system/primitives/InfoCallout/{InfoCallout.tsx,InfoCallout.module.css}`
- Create: `src/design-system/primitives/EmptyState/{EmptyState.tsx,EmptyState.module.css}`
- Create: `src/design-system/primitives/CodeBlock/{CodeBlock.tsx,CodeBlock.module.css,CodeBlock.test.tsx}`

- [ ] **Step 1: Implement InfoCallout** — block with 3px left border (amber default, variant colors), light background.

- [ ] **Step 2: Implement EmptyState** — centered icon + title + description + optional action button.

- [ ] **Step 3: Write CodeBlock test**

```tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { CodeBlock } from './CodeBlock'

describe('CodeBlock', () => {
  it('renders content in a pre element', () => {
    render(<CodeBlock content='{"key": "value"}' />)
    expect(screen.getByText(/"key"/)).toBeInTheDocument()
  })

  it('pretty-prints JSON when language is json', () => {
    render(<CodeBlock content='{"a":1}' language="json" />)
    expect(screen.getByText(/"a":/)).toBeInTheDocument()
  })

  it('shows copy button when copyable', () => {
    render(<CodeBlock content="test" copyable />)
    expect(screen.getByRole('button', { name: /copy/i })).toBeInTheDocument()
  })
})
```

- [ ] **Step 4: Implement CodeBlock** — `<pre>` with mono font, `--bg-inset` background, optional line numbers gutter, optional copy button, auto-JSON-pretty-print.

- [ ] **Step 5: Run tests and commit**

```bash
npx vitest run src/design-system/primitives/CodeBlock/
git add src/design-system/primitives/InfoCallout/ src/design-system/primitives/EmptyState/ src/design-system/primitives/CodeBlock/
git commit -m "feat: InfoCallout, EmptyState, CodeBlock primitives"
```

---

### Task 12: Collapsible, Tooltip

**Files:**
- Create: `src/design-system/primitives/Collapsible/{Collapsible.tsx,Collapsible.module.css,Collapsible.test.tsx}`
- Create: `src/design-system/primitives/Tooltip/{Tooltip.tsx,Tooltip.module.css}`

Note: Sparkline was moved to Task 10 (dependency of StatCard).

- [ ] **Step 1: Write Collapsible test**

```tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Collapsible } from './Collapsible'

describe('Collapsible', () => {
  it('renders title', () => {
    render(<Collapsible title="Details">Content</Collapsible>)
    expect(screen.getByText('Details')).toBeInTheDocument()
  })

  it('hides content by default', () => {
    render(<Collapsible title="Details">Hidden content</Collapsible>)
    expect(screen.queryByText('Hidden content')).not.toBeVisible()
  })

  it('shows content when defaultOpen', () => {
    render(<Collapsible title="Details" defaultOpen>Visible content</Collapsible>)
    expect(screen.getByText('Visible content')).toBeVisible()
  })

  it('toggles content on click', async () => {
    const user = userEvent.setup()
    render(<Collapsible title="Details">Content</Collapsible>)
    await user.click(screen.getByText('Details'))
    expect(screen.getByText('Content')).toBeVisible()
  })

  it('calls onToggle when toggled', async () => {
    const onToggle = vi.fn()
    const user = userEvent.setup()
    render(<Collapsible title="Details" onToggle={onToggle}>Content</Collapsible>)
    await user.click(screen.getByText('Details'))
    expect(onToggle).toHaveBeenCalled()
  })
})
```

- [ ] **Step 2: Implement Collapsible** — controlled (`open` prop) / uncontrolled (`defaultOpen` prop), animated height transition via `max-height` + `overflow: hidden` + CSS transition. Title acts as toggle trigger with chevron indicator.

- [ ] **Step 3: Implement Tooltip** — CSS-only hover popup positioned with `position: absolute` relative to the wrapper. Four positions: top/bottom/left/right.

- [ ] **Step 4: Run tests and commit**

```bash
npx vitest run src/design-system/primitives/Collapsible/
git add src/design-system/primitives/Collapsible/ src/design-system/primitives/Tooltip/
git commit -m "feat: Collapsible and Tooltip primitives"
```

---

### Task 13: DateTimePicker, DateRangePicker

**Files:**
- Create: `src/design-system/primitives/DateTimePicker/{DateTimePicker.tsx,DateTimePicker.module.css}`
- Create: `src/design-system/primitives/DateRangePicker/{DateRangePicker.tsx,DateRangePicker.module.css,DateRangePicker.test.tsx}`

- [ ] **Step 1: Implement DateTimePicker** — styled native `<input type="datetime-local">` wrapped in Input-like container with same focus ring styling.

- [ ] **Step 2: Write DateRangePicker test**

```tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { DateRangePicker } from './DateRangePicker'

describe('DateRangePicker', () => {
  it('renders two datetime inputs', () => {
    const { container } = render(
      <DateRangePicker
        value={{ start: new Date(), end: new Date() }}
        onChange={() => {}}
      />,
    )
    const inputs = container.querySelectorAll('input[type="datetime-local"]')
    expect(inputs.length).toBe(2)
  })

  it('renders preset buttons', () => {
    render(
      <DateRangePicker
        value={{ start: new Date(), end: new Date() }}
        onChange={() => {}}
      />,
    )
    expect(screen.getByText('Last 1h')).toBeInTheDocument()
    expect(screen.getByText('Today')).toBeInTheDocument()
  })

  it('calls onChange when a preset is clicked', async () => {
    const onChange = vi.fn()
    const user = userEvent.setup()
    render(
      <DateRangePicker
        value={{ start: new Date(), end: new Date() }}
        onChange={onChange}
      />,
    )
    await user.click(screen.getByText('Last 1h'))
    expect(onChange).toHaveBeenCalledWith(
      expect.objectContaining({ start: expect.any(Date), end: expect.any(Date) }),
    )
  })
})
```

- [ ] **Step 3: Implement DateRangePicker** — two DateTimePicker inputs side by side with presets row above (FilterPill chips for each preset). Clicking a preset computes start/end dates and calls `onChange`.

Default presets:
```ts
const DEFAULT_PRESETS = [
  { label: 'Last 1h', value: 'last-1h' },
  { label: 'Last 6h', value: 'last-6h' },
  { label: 'Today', value: 'today' },
  { label: 'This shift', value: 'shift' },
  { label: 'Last 24h', value: 'last-24h' },
  { label: 'Last 7d', value: 'last-7d' },
  { label: 'Custom', value: 'custom' },
]
```

- [ ] **Step 4: Run tests, verify build, and commit**

```bash
npx tsc --noEmit
git add src/design-system/primitives/DateTimePicker/ src/design-system/primitives/DateRangePicker/
git commit -m "feat: DateTimePicker and DateRangePicker primitives"
```

---

### Task 14: Primitives Barrel Export

**Files:**
- Create: `src/design-system/primitives/index.ts`

- [ ] **Step 1: Create barrel export**

```ts
export { Avatar } from './Avatar/Avatar'
export { Badge } from './Badge/Badge'
export { Button } from './Button/Button'
export { Card } from './Card/Card'
export { Checkbox } from './Checkbox/Checkbox'
export { CodeBlock } from './CodeBlock/CodeBlock'
export { Collapsible } from './Collapsible/Collapsible'
export { DateTimePicker } from './DateTimePicker/DateTimePicker'
export { DateRangePicker } from './DateRangePicker/DateRangePicker'
export { EmptyState } from './EmptyState/EmptyState'
export { FilterPill } from './FilterPill/FilterPill'
export { InfoCallout } from './InfoCallout/InfoCallout'
export { Input } from './Input/Input'
export { KeyboardHint } from './KeyboardHint/KeyboardHint'
export { MonoText } from './MonoText/MonoText'
export { SectionHeader } from './SectionHeader/SectionHeader'
export { Select } from './Select/Select'
export { Sparkline } from './Sparkline/Sparkline'
export { Spinner } from './Spinner/Spinner'
export { StatCard } from './StatCard/StatCard'
export { StatusDot } from './StatusDot/StatusDot'
export { Tag } from './Tag/Tag'
export { Toggle } from './Toggle/Toggle'
export { Tooltip } from './Tooltip/Tooltip'
```

- [ ] **Step 2: Run all primitive tests**

```bash
npx vitest run src/design-system/primitives/
```

Expected: All tests pass.

- [ ] **Step 3: Commit**

```bash
git add src/design-system/primitives/index.ts
git commit -m "feat: primitives barrel export"
```

---

## Phase 3: Composites

### Task 15: Breadcrumb + Tabs

**Files:**
- Create: `src/design-system/composites/Breadcrumb/{Breadcrumb.tsx,Breadcrumb.module.css}`
- Create: `src/design-system/composites/Tabs/{Tabs.tsx,Tabs.module.css,Tabs.test.tsx}`

- [ ] **Step 1: Implement Breadcrumb** — `nav > ol > li` with `/` separators, last item bold. Reference mock lines 412-421.

- [ ] **Step 2: Write Tabs test**

```tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Tabs } from './Tabs'

describe('Tabs', () => {
  const tabs = [
    { label: 'All', count: 14, value: 'all' },
    { label: 'Executions', count: 8, value: 'executions' },
    { label: 'Routes', count: 3, value: 'routes' },
  ]

  it('renders all tab labels', () => {
    render(<Tabs tabs={tabs} active="all" onChange={() => {}} />)
    expect(screen.getByText('All')).toBeInTheDocument()
    expect(screen.getByText('Executions')).toBeInTheDocument()
  })

  it('shows count badges', () => {
    render(<Tabs tabs={tabs} active="all" onChange={() => {}} />)
    expect(screen.getByText('14')).toBeInTheDocument()
  })

  it('calls onChange with tab value', async () => {
    const onChange = vi.fn()
    const user = userEvent.setup()
    render(<Tabs tabs={tabs} active="all" onChange={onChange} />)
    await user.click(screen.getByText('Routes'))
    expect(onChange).toHaveBeenCalledWith('routes')
  })
})
```

- [ ] **Step 3: Implement Tabs** — horizontal bar with underline active indicator, optional count badges.

- [ ] **Step 4: Run tests and commit**

```bash
npx vitest run src/design-system/composites/Tabs/
git add src/design-system/composites/Breadcrumb/ src/design-system/composites/Tabs/
git commit -m "feat: Breadcrumb and Tabs composites"
```

---

### Task 16: MenuItem + Dropdown

**Files:**
- Create: `src/design-system/composites/MenuItem/{MenuItem.tsx,MenuItem.module.css}`
- Create: `src/design-system/composites/Dropdown/{Dropdown.tsx,Dropdown.module.css,Dropdown.test.tsx}`

- [ ] **Step 1: Implement MenuItem** — sidebar nav item with StatusDot + label + meta + count. Reference mock lines 270-312 for exact CSS (left border active state, hover, indentation).

- [ ] **Step 2: Write Dropdown test**

```tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Dropdown } from './Dropdown'

const items = [
  { label: 'Edit', onClick: vi.fn() },
  { label: 'Delete', onClick: vi.fn() },
]

describe('Dropdown', () => {
  it('does not show menu initially', () => {
    render(<Dropdown trigger={<button>Actions</button>} items={items} />)
    expect(screen.queryByText('Edit')).not.toBeInTheDocument()
  })

  it('shows menu on trigger click', async () => {
    const user = userEvent.setup()
    render(<Dropdown trigger={<button>Actions</button>} items={items} />)
    await user.click(screen.getByText('Actions'))
    expect(screen.getByText('Edit')).toBeInTheDocument()
  })

  it('closes on Esc', async () => {
    const user = userEvent.setup()
    render(<Dropdown trigger={<button>Actions</button>} items={items} />)
    await user.click(screen.getByText('Actions'))
    await user.keyboard('{Escape}')
    expect(screen.queryByText('Edit')).not.toBeInTheDocument()
  })

  it('calls item onClick when clicked', async () => {
    const user = userEvent.setup()
    render(<Dropdown trigger={<button>Actions</button>} items={items} />)
    await user.click(screen.getByText('Actions'))
    await user.click(screen.getByText('Edit'))
    expect(items[0].onClick).toHaveBeenCalled()
  })
})
```

- [ ] **Step 3: Implement Dropdown** — trigger element + floating `<ul>` menu. Toggle on click, close on outside click or Esc. Items with optional icons and dividers.

- [ ] **Step 4: Run tests and commit**

```bash
git add src/design-system/composites/MenuItem/ src/design-system/composites/Dropdown/
git commit -m "feat: MenuItem and Dropdown composites"
```

---

### Task 17: Modal + DetailPanel

**Files:**
- Create: `src/design-system/composites/Modal/{Modal.tsx,Modal.module.css,Modal.test.tsx}`
- Create: `src/design-system/composites/DetailPanel/{DetailPanel.tsx,DetailPanel.module.css}`

- [ ] **Step 1: Write Modal test**

```tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Modal } from './Modal'

describe('Modal', () => {
  it('renders children when open', () => {
    render(<Modal open onClose={() => {}}>Content</Modal>)
    expect(screen.getByText('Content')).toBeInTheDocument()
  })

  it('does not render when closed', () => {
    render(<Modal open={false} onClose={() => {}}>Content</Modal>)
    expect(screen.queryByText('Content')).not.toBeInTheDocument()
  })

  it('renders title when provided', () => {
    render(<Modal open onClose={() => {}} title="My Modal">Content</Modal>)
    expect(screen.getByText('My Modal')).toBeInTheDocument()
  })

  it('calls onClose on Esc', async () => {
    const onClose = vi.fn()
    const user = userEvent.setup()
    render(<Modal open onClose={onClose}>Content</Modal>)
    await user.keyboard('{Escape}')
    expect(onClose).toHaveBeenCalled()
  })

  it('calls onClose on backdrop click', async () => {
    const onClose = vi.fn()
    const user = userEvent.setup()
    render(<Modal open onClose={onClose}>Content</Modal>)
    const backdrop = screen.getByTestId('modal-backdrop')
    await user.click(backdrop)
    expect(onClose).toHaveBeenCalled()
  })
})
```

- [ ] **Step 2: Implement Modal** — overlay with backdrop (clicks close), centered Card content, close on Esc. Uses `createPortal` to render at document body. Size variants: `sm` (400px), `md` (560px), `lg` (720px). Add `data-testid="modal-backdrop"` to the backdrop element.

- [ ] **Step 3: Implement DetailPanel** — 400px right-side sliding panel. Animated `slideInRight`. Header with title + close button, Tabs for content switching, bottom action bar. Reference mock lines 1078-1179 for CSS.

- [ ] **Step 4: Run tests and commit**

```bash
npx vitest run src/design-system/composites/Modal/
git add src/design-system/composites/Modal/ src/design-system/composites/DetailPanel/
git commit -m "feat: Modal and DetailPanel composites"
```

```bash
git add src/design-system/composites/Modal/ src/design-system/composites/DetailPanel/
git commit -m "feat: Modal and DetailPanel composites"
```

---

### Task 18: FilterBar + ShortcutsBar

**Files:**
- Create: `src/design-system/composites/FilterBar/{FilterBar.tsx,FilterBar.module.css}`
- Create: `src/design-system/composites/ShortcutsBar/{ShortcutsBar.tsx,ShortcutsBar.module.css}`

- [ ] **Step 1: Implement FilterBar** — composable row of: Input (search) + separator + FilterPill group + active filter Tags below. Reference mock lines 614-773.

- [ ] **Step 2: Implement ShortcutsBar** — fixed bottom-right position, horizontal row of `KeyboardHint + label` pairs. Reference mock lines 1315-1346.

- [ ] **Step 3: Commit**

```bash
git add src/design-system/composites/FilterBar/ src/design-system/composites/ShortcutsBar/
git commit -m "feat: FilterBar and ShortcutsBar composites"
```

---

### Task 19: DataTable

**Files:**
- Create: `src/design-system/composites/DataTable/{DataTable.tsx,DataTable.module.css,DataTable.test.tsx,types.ts}`

This is one of the most complex composites. Dedicated task.

- [ ] **Step 1: Define types**

Create `src/design-system/composites/DataTable/types.ts`:
```ts
import type { ReactNode } from 'react'

export interface Column<T = unknown> {
  key: string
  header: string
  width?: string
  sortable?: boolean
  render?: (value: unknown, row: T) => ReactNode
}

export interface DataTableProps<T extends { id: string }> {
  columns: Column<T>[]
  data: T[]
  onRowClick?: (row: T) => void
  selectedId?: string
  sortable?: boolean
  pageSize?: number
  pageSizeOptions?: number[]
  rowAccent?: (row: T) => 'error' | 'warning' | undefined
  expandedContent?: (row: T) => ReactNode | null
}
```

- [ ] **Step 2: Write DataTable test**

```tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { DataTable } from './DataTable'

const columns = [
  { key: 'name', header: 'Name' },
  { key: 'status', header: 'Status' },
]

const data = [
  { id: '1', name: 'Route A', status: 'ok' },
  { id: '2', name: 'Route B', status: 'error' },
  { id: '3', name: 'Route C', status: 'ok' },
]

describe('DataTable', () => {
  it('renders column headers', () => {
    render(<DataTable columns={columns} data={data} />)
    expect(screen.getByText('Name')).toBeInTheDocument()
    expect(screen.getByText('Status')).toBeInTheDocument()
  })

  it('renders all data rows', () => {
    render(<DataTable columns={columns} data={data} />)
    expect(screen.getByText('Route A')).toBeInTheDocument()
    expect(screen.getByText('Route C')).toBeInTheDocument()
  })

  it('calls onRowClick when a row is clicked', async () => {
    const onRowClick = vi.fn()
    const user = userEvent.setup()
    render(<DataTable columns={columns} data={data} onRowClick={onRowClick} />)
    await user.click(screen.getByText('Route B'))
    expect(onRowClick).toHaveBeenCalledWith(data[1])
  })

  it('highlights selected row', () => {
    const { container } = render(
      <DataTable columns={columns} data={data} selectedId="2" />,
    )
    const rows = container.querySelectorAll('tr')
    expect(rows[2]).toHaveClass('selected') // row index 2 = data[1]
  })

  it('sorts when header clicked', async () => {
    const user = userEvent.setup()
    render(<DataTable columns={[{ key: 'name', header: 'Name', sortable: true }, ...columns.slice(1)]} data={data} sortable />)
    await user.click(screen.getByText('Name'))
    // After click, should show sort indicator
    expect(screen.getByText('Name').closest('th')).toHaveAttribute('aria-sort')
  })
})
```

- [ ] **Step 3: Implement DataTable** — `<table>` with: sortable headers (click toggles asc/desc, arrow icon), 40px compact rows, row selection class, optional left accent border via `rowAccent`, optional `expandedContent` below row, pagination bar at bottom with page info + page size `<Select>` + prev/next `<Button>`.

Reference `ui-mocks/mock-v2-light.html` lines 776-1076 for table CSS.

- [ ] **Step 4: Run tests**

```bash
npx vitest run src/design-system/composites/DataTable/
```

- [ ] **Step 5: Commit**

```bash
git add src/design-system/composites/DataTable/
git commit -m "feat: DataTable composite with sorting, pagination, row selection"
```

---

### Task 20: AreaChart, LineChart, BarChart

**Files:**
- Create: `src/design-system/composites/AreaChart/{AreaChart.tsx,AreaChart.module.css}`
- Create: `src/design-system/composites/LineChart/{LineChart.tsx,LineChart.module.css}`
- Create: `src/design-system/composites/BarChart/{BarChart.tsx,BarChart.module.css}`

All charts are SVG-based, no external libraries.

- [ ] **Step 1: Create shared chart utilities**

Create `src/design-system/composites/_chart-utils.ts`:
```ts
export function computeScale(data: number[], height: number, padding: number) {
  const min = Math.min(...data)
  const max = Math.max(...data)
  const range = max - min || 1
  return {
    min, max, range,
    toY: (val: number) => height - padding - ((val - min) / range) * (height - 2 * padding),
  }
}

export function formatAxisLabel(val: number): string {
  if (val >= 1000) return `${(val / 1000).toFixed(1)}k`
  return String(val)
}
```

- [ ] **Step 2: Implement AreaChart** — SVG with: Y axis labels (left), X axis labels (bottom), grid lines (dashed, `--border-subtle`), area fill (colored with 0.1 opacity), line stroke, optional threshold line (dashed, labeled). Multi-series using `--chart-N` tokens. Hover crosshair + tooltip.

Reference `ui-mocks/mock-v3-metrics-dashboard.html` for chart layout patterns.

- [ ] **Step 3: Implement LineChart** — same as AreaChart but without area fill. Reuse chart utilities.

- [ ] **Step 4: Implement BarChart** — SVG vertical bars. Stacked or grouped mode. Same axis/grid pattern.

- [ ] **Step 5: Commit**

```bash
git add src/design-system/composites/AreaChart/ src/design-system/composites/LineChart/ src/design-system/composites/BarChart/ src/design-system/composites/_chart-utils.ts
git commit -m "feat: AreaChart, LineChart, BarChart SVG composites"
```

---

### Task 21: ProcessorTimeline + EventFeed

**Files:**
- Create: `src/design-system/composites/ProcessorTimeline/{ProcessorTimeline.tsx,ProcessorTimeline.module.css}`
- Create: `src/design-system/composites/EventFeed/{EventFeed.tsx,EventFeed.module.css}`

- [ ] **Step 1: Implement ProcessorTimeline** — Gantt-style horizontal bars. Each row: processor name label (120px), colored bar proportional to duration, duration label. Bar colors: `--success` for ok, `--warning` for slow, `--error` for fail. Clickable rows. Reference mock lines 1145-1232 (`.proc-row`, `.proc-bar-bg`, `.proc-bar-fill`).

- [ ] **Step 2: Implement EventFeed** — scrolling list inside a Card. Each event: severity icon (StatusDot), message text, relative timestamp. Auto-scroll to bottom on new events (pause on manual scroll). Optional severity filter pills at top.

- [ ] **Step 3: Commit**

```bash
git add src/design-system/composites/ProcessorTimeline/ src/design-system/composites/EventFeed/
git commit -m "feat: ProcessorTimeline and EventFeed composites"
```

---

### Task 22: CommandPalette

**Files:**
- Create: `src/design-system/composites/CommandPalette/{CommandPalette.tsx,CommandPalette.module.css,CommandPalette.test.tsx,types.ts}`

Complex composite — dedicated task.

- [ ] **Step 1: Define types**

Create `src/design-system/composites/CommandPalette/types.ts`:
```ts
import type { ReactNode } from 'react'

export type SearchCategory = 'execution' | 'route' | 'exchange' | 'agent'

export interface SearchResult {
  id: string
  category: SearchCategory
  title: string
  badges?: { label: string; color?: string }[]
  meta: string
  timestamp?: string
  icon?: ReactNode
  expandedContent?: string
  matchRanges?: [number, number][]
}

export interface ScopeFilter {
  field: string
  value: string
}
```

- [ ] **Step 2: Write CommandPalette test**

Test: renders open/closed, keyboard navigation (arrow keys, esc closes), category tabs filter results, scoped filter tags.

- [ ] **Step 3: Implement CommandPalette**

Structure:
- Portal-rendered overlay (Modal-like backdrop)
- Search input area: search icon + ScopeFilter tags (removable) + text input + `esc close` hint
- Category Tabs below search (All, Executions, Routes, Exchanges, Agents) with counts
- Grouped result list: SectionHeader per category, result items below
- Result item: icon + title (with match highlighting) + badges + meta + timestamp
- Expandable CodeBlock for `expandedContent`
- Bottom ShortcutsBar (arrows navigate, enter open, tab filter, # scope)
- Keyboard: `ArrowUp`/`ArrowDown` navigate results, `Enter` selects, `Esc` closes, `Tab` cycles scope
- Match highlighting: wrap matched ranges in `<mark>` styled with amber

Reference the Cmd+K screenshot for exact layout.

- [ ] **Step 4: Register global Ctrl+K handler**

The CommandPalette should include a `useEffect` that listens for `Ctrl+K` / `Cmd+K` on the window and calls `onOpen`. This can be wired at the App level.

- [ ] **Step 5: Run tests and commit**

```bash
npx vitest run src/design-system/composites/CommandPalette/
git add src/design-system/composites/CommandPalette/
git commit -m "feat: CommandPalette composite with search, filtering, keyboard navigation"
```

---

### Task 23: Composites Barrel Export

**Files:**
- Create: `src/design-system/composites/index.ts`

- [ ] **Step 1: Create barrel export**

```ts
export { AreaChart } from './AreaChart/AreaChart'
export { BarChart } from './BarChart/BarChart'
export { Breadcrumb } from './Breadcrumb/Breadcrumb'
export { CommandPalette } from './CommandPalette/CommandPalette'
export { DataTable } from './DataTable/DataTable'
export { DetailPanel } from './DetailPanel/DetailPanel'
export { Dropdown } from './Dropdown/Dropdown'
export { EventFeed } from './EventFeed/EventFeed'
export { FilterBar } from './FilterBar/FilterBar'
export { LineChart } from './LineChart/LineChart'
export { MenuItem } from './MenuItem/MenuItem'
export { Modal } from './Modal/Modal'
export { ProcessorTimeline } from './ProcessorTimeline/ProcessorTimeline'
export { ShortcutsBar } from './ShortcutsBar/ShortcutsBar'
export { Tabs } from './Tabs/Tabs'
```

- [ ] **Step 2: Run all composite tests**

```bash
npx vitest run src/design-system/composites/
```

- [ ] **Step 3: Commit**

```bash
git add src/design-system/composites/index.ts
git commit -m "feat: composites barrel export"
```

---

## Phase 4: Layout

### Task 24: AppShell, Sidebar, TopBar

**Files:**
- Create: `src/design-system/layout/AppShell/{AppShell.tsx,AppShell.module.css}`
- Create: `src/design-system/layout/Sidebar/{Sidebar.tsx,Sidebar.module.css}`
- Create: `src/design-system/layout/TopBar/{TopBar.tsx,TopBar.module.css}`
- Create: `src/design-system/layout/index.ts`

- [ ] **Step 1: Implement AppShell**

```tsx
// AppShell.tsx
import styles from './AppShell.module.css'
import type { ReactNode } from 'react'

interface AppShellProps {
  sidebar: ReactNode
  children: ReactNode
  detail?: ReactNode
}

export function AppShell({ sidebar, children, detail }: AppShellProps) {
  return (
    <div className={styles.app}>
      {sidebar}
      <div className={styles.main}>
        {children}
      </div>
      {detail}
    </div>
  )
}
```

```css
/* AppShell.module.css */
.app {
  display: flex;
  height: 100vh;
  overflow: hidden;
}

.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-width: 0;
}
```

- [ ] **Step 2: Implement Sidebar** — 220px warm charcoal. Sections: logo, search, Applications section (MenuItem list), divider, Routes section (indented MenuItems), Agent health section (agent items with dot, name, version, tps, last-seen), bottom links. Reference mock lines 190-503 for exact structure and CSS.

- [ ] **Step 3: Implement TopBar** — 48px height. Breadcrumb left, search trigger center, environment badge + shift indicator + Avatar + name right. Reference mock lines 400-501.

- [ ] **Step 4: Create layout barrel export**

```ts
export { AppShell } from './AppShell/AppShell'
export { Sidebar } from './Sidebar/Sidebar'
export { TopBar } from './TopBar/TopBar'
```

- [ ] **Step 5: Verify build and commit**

```bash
npx tsc --noEmit
git add src/design-system/layout/
git commit -m "feat: AppShell, Sidebar, TopBar layout components"
```

---

## Phase 5: Mock Data + Pages

### Task 25: Mock Data

**Files:**
- Create: `src/mocks/executions.ts`
- Create: `src/mocks/routes.ts`
- Create: `src/mocks/agents.ts`
- Create: `src/mocks/metrics.ts`

- [ ] **Step 1: Create execution mock data**

Extract realistic data from `ui-mocks/mock-v2-light.html` body. Include ~15 execution rows with mixed statuses, order IDs, durations, error messages.

```ts
// executions.ts
export interface Execution {
  id: string
  orderId: string
  customer: string
  route: string
  status: 'completed' | 'failed' | 'running' | 'warning'
  durationMs: number
  timestamp: Date
  correlationId: string
  errorMessage?: string
  errorClass?: string
  processors: {
    name: string
    type: string
    durationMs: number
    status: 'ok' | 'slow' | 'fail'
    startMs: number
  }[]
}

export const executions: Execution[] = [
  {
    id: 'E-2026-03-18-00142',
    orderId: 'OP-88421',
    customer: 'Acme Corp',
    route: 'order-intake',
    status: 'completed',
    durationMs: 142,
    timestamp: new Date('2026-03-18T10:32:15'),
    correlationId: 'cmr-9a4f2b71-e8c3',
    processors: [
      { name: 'from(jms:orders)', type: 'consumer', durationMs: 5, status: 'ok', startMs: 0 },
      { name: 'unmarshal(json)', type: 'transform', durationMs: 8, status: 'ok', startMs: 5 },
      { name: 'validate(schema)', type: 'process', durationMs: 12, status: 'ok', startMs: 13 },
      { name: 'enrich(inventory)', type: 'enrich', durationMs: 85, status: 'slow', startMs: 25 },
      { name: 'to(payment-api)', type: 'to', durationMs: 28, status: 'ok', startMs: 110 },
    ],
  },
  // ... add ~14 more rows with varied statuses, some failed with error messages
]
```

- [ ] **Step 2: Create routes, agents, metrics mock data** — follow same pattern, extract from mockup data.

- [ ] **Step 3: Commit**

```bash
git add src/mocks/
git commit -m "feat: static mock data for executions, routes, agents, metrics"
```

---

### Task 26: Dashboard Page

**Files:**
- Create: `src/pages/Dashboard/Dashboard.tsx`
- Create: `src/pages/Dashboard/Dashboard.module.css`

- [ ] **Step 1: Implement Dashboard page**

Compose from design system components:
- `AppShell` wrapping `Sidebar` + main content + `DetailPanel`
- `TopBar` with breadcrumb "Applications / order-service"
- Health strip: 5x `StatCard` in a CSS grid (executions, success rate, errors, throughput, latency)
- `FilterBar` with status pills + search
- `DataTable` for execution list
- `DetailPanel` slides in on row click with tabs (Overview, Processors, Exchange, Error)
- `ShortcutsBar` at bottom
- CommandPalette triggered by Ctrl+K

All data sourced from `src/mocks/`.

Reference: `ui-mocks/mock-v2-light.html` for exact layout and data.

- [ ] **Step 2: Wire up in App.tsx**

```tsx
import { Routes, Route } from 'react-router-dom'
import { Dashboard } from './pages/Dashboard/Dashboard'

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />} />
    </Routes>
  )
}
```

- [ ] **Step 3: Visual verification**

```bash
npm run dev
```

Open browser, verify the dashboard matches `ui-mocks/mock-v2-light.html` layout. Check: sidebar, topbar, health strip, filter bar, table, detail panel interaction.

- [ ] **Step 4: Commit**

```bash
git add src/pages/Dashboard/ src/App.tsx
git commit -m "feat: Dashboard page composing full design system"
```

---

### Task 27: Metrics Page

**Files:**
- Create: `src/pages/Metrics/Metrics.tsx`
- Create: `src/pages/Metrics/Metrics.module.css`

- [ ] **Step 1: Implement Metrics page**

Reference: `ui-mocks/mock-v3-metrics-dashboard.html`

Compose: AppShell layout + TopBar (breadcrumb: Dashboard > Metrics) + DateRangePicker bar + 5 StatCards with sparklines + per-route DataTable with sparkline columns + 2x2 chart grid (AreaChart for throughput, LineChart for latency, BarChart for errors, AreaChart for volume) + optional EventFeed right rail.

- [ ] **Step 2: Add route to App.tsx**

```tsx
<Route path="/metrics" element={<Metrics />} />
```

- [ ] **Step 3: Commit**

```bash
git add src/pages/Metrics/ src/App.tsx
git commit -m "feat: Metrics dashboard page"
```

---

### Task 28: RouteDetail Page

**Files:**
- Create: `src/pages/RouteDetail/RouteDetail.tsx`
- Create: `src/pages/RouteDetail/RouteDetail.module.css`

- [ ] **Step 1: Implement RouteDetail page**

Reference: `ui-mocks/mock-v3-route-detail.html`

Compose: route header (name, status, uptime) + KPI strip (executions, success rate, latencies, inflight) + ProcessorTimeline aggregate view + filtered DataTable + error patterns section.

- [ ] **Step 2: Add route and commit**

```bash
git add src/pages/RouteDetail/ src/App.tsx
git commit -m "feat: RouteDetail page"
```

---

### Task 29: ExchangeDetail + AgentHealth Pages

**Files:**
- Create: `src/pages/ExchangeDetail/ExchangeDetail.tsx`
- Create: `src/pages/ExchangeDetail/ExchangeDetail.module.css`
- Create: `src/pages/AgentHealth/AgentHealth.tsx`
- Create: `src/pages/AgentHealth/AgentHealth.module.css`

- [ ] **Step 1: Implement ExchangeDetail page**

Reference: `ui-mocks/mock-v3-exchange-detail.html`

Compose: exchange header + ProcessorTimeline + step-by-step exchange inspector using CodeBlock + Collapsible for each processor step + error block.

- [ ] **Step 2: Implement AgentHealth page**

Reference: `ui-mocks/mock-v3-agent-health.html`

Compose: agent cards grid + per-agent detail panel + LineCharts for throughput/error rate trends.

- [ ] **Step 3: Add routes and commit**

```bash
git add src/pages/ExchangeDetail/ src/pages/AgentHealth/ src/App.tsx
git commit -m "feat: ExchangeDetail and AgentHealth pages"
```

---

### Task 30: Final Integration + Run All Tests

- [ ] **Step 1: Run full test suite**

```bash
npx vitest run
```

Expected: All tests pass.

- [ ] **Step 2: Run TypeScript check**

```bash
npx tsc --noEmit
```

Expected: No type errors.

- [ ] **Step 3: Build for production**

```bash
npm run build
```

Expected: Clean build, no warnings.

- [ ] **Step 4: Visual smoke test**

```bash
npm run dev
```

Navigate through all pages: `/`, `/metrics`, `/routes/order-intake`, `/exchanges/E-2026-03-18-00142`, `/agents`. Verify: sidebar navigation, theme toggle (add a temp button or use devtools to set `data-theme="dark"`), CommandPalette opens with Ctrl+K.

- [ ] **Step 5: Final commit**

```bash
git add -A
git commit -m "feat: Cameleer3 design system — complete standalone implementation"
```
