Files
design-system/docs/superpowers/plans/2026-03-18-design-system.md
hsiegeln e5c3679b9e plan: Cameleer3 design system implementation plan (30 tasks)
Covers: scaffold, foundations, 23 primitives, 15 composites,
3 layout components, mock data, and 5 pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 08:56:13 +01:00

72 KiB

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:

cd C:/Users/Hendrik/Documents/projects/design-system
git init
echo "* text=auto eol=lf" > .gitattributes
  • Step 1: Initialize Vite project
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
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
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:

import '@testing-library/jest-dom/vitest'
  • Step 5: Update tsconfig.json

Ensure compilerOptions includes:

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}
  • Step 6: Update index.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
npm run dev

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

  • Step 8: Commit
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:

: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:

*, *::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:

@import './design-system/tokens.css';
@import './design-system/reset.css';
  • Step 4: Wire up main.tsx

Create src/main.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:

export default function App() {
  return <div>Cameleer3 Design System</div>
}
  • Step 6: Commit
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:

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
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:

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
npx vitest run src/design-system/utils/hashColor.test.ts

Expected: All 7 tests PASS.

  • Step 5: Commit
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:

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
npx vitest run src/design-system/providers/ThemeProvider.test.tsx
  • Step 3: Implement ThemeProvider

Create src/design-system/providers/ThemeProvider.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
npx vitest run src/design-system/providers/ThemeProvider.test.tsx

Expected: All 5 tests PASS.

  • Step 5: Commit
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:

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:

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:

.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:

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:

.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
npx vitest run src/design-system/primitives/StatusDot/

Expected: All 4 tests PASS.

  • Step 5: Commit
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
// 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>
}
/* 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
// 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>
}
/* 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
// 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>
  )
}
/* 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
npx tsc --noEmit
  • Step 5: Commit
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

// 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
// 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>
  )
}
/* 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
// 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'
/* 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
npx vitest run src/design-system/primitives/Button/

Expected: All 4 tests PASS.

  • Step 5: Commit
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

npx tsc --noEmit
  • Step 5: Commit
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
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
// 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>
  )
}
/* 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).

// 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>
  )
}
/* 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.

// 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>
  )
}
/* 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
npx vitest run src/design-system/primitives/Badge/ src/design-system/primitives/Avatar/
  • Step 6: Commit
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)

// 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:

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

npx tsc --noEmit
  • Step 5: Commit
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

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

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
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

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

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:

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
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

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
npx vitest run src/design-system/primitives/

Expected: All tests pass.

  • Step 3: Commit
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 Breadcrumbnav > ol > li with / separators, last item bold. Reference mock lines 412-421.

  • Step 2: Write Tabs test

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

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

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

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

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

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"
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

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:

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
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
npx vitest run src/design-system/composites/DataTable/
  • Step 5: Commit
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:

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

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

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:

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
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

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
npx vitest run src/design-system/composites/
  • Step 3: Commit
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

// 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>
  )
}
/* 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

export { AppShell } from './AppShell/AppShell'
export { Sidebar } from './Sidebar/Sidebar'
export { TopBar } from './TopBar/TopBar'
  • Step 5: Verify build and commit
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.

// 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

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
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
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
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
<Route path="/metrics" element={<Metrics />} />
  • Step 3: Commit
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
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
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
npx vitest run

Expected: All tests pass.

  • Step 2: Run TypeScript check
npx tsc --noEmit

Expected: No type errors.

  • Step 3: Build for production
npm run build

Expected: Clean build, no warnings.

  • Step 4: Visual smoke test
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
git add -A
git commit -m "feat: Cameleer3 design system — complete standalone implementation"