# 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 Cameleer3
``` - [ ] **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( , ) ``` - [ ] **Step 5: Create placeholder App.tsx** Create `src/App.tsx`: ```tsx export default function App() { return
Cameleer3 Design System
} ``` - [ ] **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 (
{theme}
) } describe('ThemeProvider', () => { beforeEach(() => { localStorage.clear() document.documentElement.removeAttribute('data-theme') }) it('defaults to light theme', () => { render( , ) expect(screen.getByTestId('theme').textContent).toBe('light') }) it('sets data-theme attribute on document', () => { render( , ) expect(document.documentElement.dataset.theme).toBe('light') }) it('toggles theme on button click', async () => { const user = userEvent.setup() render( , ) 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( , ) 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( , ) 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(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(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 ( {children} ) } 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() expect(container.firstChild).toBeInTheDocument() }) it('applies variant class', () => { const { container } = render() expect(container.firstChild).toHaveClass('error') }) it('applies pulse class for live variant by default', () => { const { container } = render() expect(container.firstChild).toHaveClass('pulse') }) it('disables pulse when pulse=false', () => { const { container } = render() 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