commit dbf6a5870dfedc2dbf6e80259d643e4be022342c Author: Richie Date: Wed May 20 14:30:41 2026 +1000 Initial scaffold: React 19 + Vite + TypeScript + Tailwind CSS v4 + Storybook 10 Design system and component library for the Research Synthesiser. Includes: - Tailwind CSS v4 with @theme-based design tokens from the existing synthesiser - Storybook 10.4 with MCP, a11y, docs, and vitest addons - ESLint + Prettier with Tailwind class sorting - Button component as pipeline validation - CLAUDE.md with project principles and conventions - ARCHITECTURE.md as living architecture document - Penpot and Storybook MCP server configuration Co-Authored-By: Claude Opus 4.6 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..6cf3103 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,51 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run dev)", + "Bash(npm run build)", + "Bash(npm run storybook)", + "Bash(npm run lint)", + "Bash(npx tsc --noEmit)", + "Bash(npx storybook *)", + "Bash(npx prettier *)", + "Bash(npx vitest *)", + "Bash(node --check *)", + "Bash(npm install *)", + "Bash(npm list *)", + "Bash(npm outdated *)", + "Bash(git status *)", + "Bash(git log *)", + "Bash(git diff *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(git branch *)", + "Bash(git checkout *)", + "Bash(git push *)", + "Bash(git pull *)", + "Bash(git remote *)", + "Bash(kill %*)", + "Bash(lsof -i *)", + "WebSearch", + "WebFetch(domain:storybook.js.org)", + "WebFetch(domain:tailwindcss.com)", + "WebFetch(domain:react.dev)", + "WebFetch(domain:vitejs.dev)", + "WebFetch(domain:vite.dev)", + "WebFetch(domain:github.com)", + "WebFetch(domain:npmjs.com)", + "WebFetch(domain:help.penpot.app)", + "WebFetch(domain:penpot.app)", + "WebFetch(domain:developer.mozilla.org)" + ] + }, + "mcpServers": { + "storybook": { + "type": "http", + "url": "http://localhost:6006/mcp" + }, + "penpot": { + "type": "http", + "url": "http://localhost:4401/mcp" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f52343a --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*storybook.log +storybook-static diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d2da38f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..7cde171 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,17 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + "stories": [ + "../src/**/*.mdx", + "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" + ], + "addons": [ + "@chromatic-com/storybook", + "@storybook/addon-vitest", + "@storybook/addon-a11y", + "@storybook/addon-docs", + "@storybook/addon-mcp" + ], + "framework": "@storybook/react-vite" +}; +export default config; \ No newline at end of file diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..b08056a --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,22 @@ +import type { Preview } from '@storybook/react-vite' +import '../src/styles/global.css' + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: 'todo' + } + }, +}; + +export default preview; \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..6c49dd5 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,112 @@ +# ARCHITECTURE.md — SDC-Frontend + +This is the living architecture document for the SDC-Frontend design system. All structural decisions are recorded here. Update this document when the architecture evolves — never let the codebase and this document drift apart. + +--- + +## 1. Overview + +SDC-Frontend is a React component library and design system that will serve as the frontend for the Research Synthesiser. It is built in phases: + +1. **Design system** — tokens, primitives, composite components +2. **Application screens** — the synthesiser UI rebuilt on top of the design system + +--- + +## 2. Token Pipeline + +``` +Penpot (design tool, self-hosted) + ↓ Penpot MCP / manual export +src/tokens/tokens.css (@theme block) + ↓ Tailwind CSS v4 reads @theme + ↓ Generates utility classes + CSS custom properties +Components (use Tailwind utilities or var() references) + ↓ Rendered in +Storybook (visual verification) +``` + +### Token Categories +- **Colours**: `--color-*` (bg, surface, border, text, primary, success, warning, error) +- **Radii**: `--radius-*` (sm, default, lg, full) +- **Shadows**: `--shadow-*` (default, md) + +### How Tailwind v4 @theme Works +Declaring `--color-primary: #2563eb` inside `@theme` in `tokens.css` automatically generates utilities like `bg-primary`, `text-primary`, `border-primary`. No JavaScript config file needed — the CSS file is the config. + +--- + +## 3. Component Taxonomy + +### `src/components/ui/` — Primitives +Atomic, reusable building blocks. Each is self-contained with no domain logic. +- Button, Input, Textarea, Select +- Card, Badge, Tag +- Dialog, Tooltip, Popover + +### `src/components/composite/` — Composed Components +Built from primitives, may carry light domain semantics. +- StatusMessage (success/error/warning/info) +- TabBar, TabPanel +- ThemeCard (maps to synthesiser's theme display) + +### `src/components/layout/` — Layout +Page-level structural components. +- AppShell (header + sidebar + content area) +- PageHeader + +--- + +## 4. Styling Approach + +- **Primary**: Tailwind utility classes +- **Conditional classes**: `cn()` from `@/lib/utils` (clsx + tailwind-merge) +- **Token values**: Always from `src/tokens/tokens.css`, never hardcoded +- **No CSS modules, no styled-components, no inline styles** (except truly dynamic values) +- **Class ordering**: Enforced by `prettier-plugin-tailwindcss` + +--- + +## 5. Storybook Conventions + +- Every component has a co-located `.stories.tsx` file +- All stories use `tags: ['autodocs']` for auto-generated docs +- Stories cover: default state, all variants, edge cases, disabled/error states +- A11y addon runs on all stories — violations should be addressed +- MCP addon enabled at `localhost:6006/mcp` for AI-assisted development + +--- + +## 6. Project Structure + +``` +src/ +├── components/ +│ ├── ui/ # Primitives +│ ├── composite/ # Composed components +│ └── layout/ # Layout components +├── tokens/ +│ └── tokens.css # Design tokens (@theme block) +├── styles/ +│ └── global.css # Tailwind imports + base styles +├── lib/ +│ └── utils.ts # cn() utility +├── hooks/ # Custom React hooks +├── pages/ # App screens (Phase 2) +├── App.tsx # Root component +└── main.tsx # Vite entry point +``` + +--- + +## 7. Design Tool Integration + +### Penpot (self-hosted at 192.168.50.211:9001) +- MCP server runs locally via `npx @penpot/mcp@stable` (port 4401) +- MCP plugin must be installed and active in the Penpot browser session +- Tools available: `high_level_overview`, `execute_code`, `penpot_api_info`, `export_shape`, `import_image` +- Design tokens in Penpot use W3C DTCG format — export as JSON, convert to `@theme` values + +### Storybook MCP +- Available at `localhost:6006/mcp` when Storybook dev server is running +- Provides: component listing, documentation retrieval, story generation, a11y testing diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..faa96c1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,150 @@ +# CLAUDE.md — SDC-Frontend Design System + +You are the lead engineer on **SDC-Frontend**, a React component library and design system for the Research Synthesiser, built for UX researchers at the NSW Department of Education. + +--- + +## How I Want You to Work + +These are non-negotiable working principles for this project. + +### Plan Before You Build +Before beginning any significant piece of work, write a plan as a markdown file in `plans/`. Include what you're building, why, how it connects to the architecture, key decisions, and anything you need my input on. Share it with me before starting. Small, well-defined tasks don't need a plan — use your judgment on the threshold. + +### Evaluate Before You Commit +Before choosing a library, pattern, or approach: identify your options, research the latest documentation (don't rely on training knowledge for version-specific details), verify it works within the project constraints, and state your reasoning — especially when you considered and rejected alternatives. + +### Ask, Don't Assume +If you need information you don't have — about infrastructure, the team's workflow, the Penpot setup, design intent, or anything unclear — ask me. Do not fill gaps with assumptions. A question costs a moment; a wrong assumption costs hours. + +### Architecture Governance +`ARCHITECTURE.md` is the living contract for this project. Before starting any task, re-read the relevant section. If you need to deviate, propose the change to me first, then update the document. If you discover something new, add it. Never let the codebase and architecture drift apart. + +### Fail Gracefully, Build Honestly +Test components with realistic props and edge cases, not only happy-path defaults. When something breaks, surface clear errors. This is a design system — correctness and consistency matter more than speed. + +--- + +## Technical Stack + +| Tool | Version | Notes | +|---|---|---| +| React | 19.x | Function components, hooks only | +| Vite | latest | Dev server + build | +| TypeScript | strict mode | All components must have typed props | +| Tailwind CSS | v4.3+ | CSS-first config via `@theme` in `src/tokens/tokens.css` | +| Storybook | 10.x | Component dev, docs, visual testing, MCP addon | +| clsx + tailwind-merge | latest | Combined as `cn()` in `src/lib/utils.ts` | +| ESLint + Prettier | latest | With `prettier-plugin-tailwindcss` for class ordering | + +--- + +## Design Tokens + +All design tokens live in `src/tokens/tokens.css` as a `@theme` block. This is the **single source of truth**. Do not hardcode colour values, radii, or shadows anywhere else. + +Use Tailwind utilities (`bg-primary`, `text-error`, `rounded-default`, etc.) or CSS variables (`var(--color-primary)`) when utilities don't cover the case. + +Token naming convention: +- Colours: `--color-{name}` (e.g., `--color-primary`, `--color-text-secondary`) +- Radii: `--radius-{size}` (e.g., `--radius-default`, `--radius-lg`) +- Shadows: `--shadow-{size}` (e.g., `--shadow-default`, `--shadow-md`) + +--- + +## Component Conventions + +### File Structure +Each component lives in its own directory: +``` +src/components/ui/Button/ + Button.tsx — Component implementation + Button.stories.tsx — Storybook stories + index.ts — Barrel export +``` + +### Component Taxonomy +- `src/components/ui/` — Primitive components (Button, Input, Card, Badge) +- `src/components/composite/` — Composed components (StatusMessage, TabBar) +- `src/components/layout/` — Layout components (AppShell, Sidebar) + +### TypeScript +- Export a named props interface: `ButtonProps`, `CardProps`, etc. +- Use `React.forwardRef` for components that wrap native elements. +- Extend native element props where appropriate (e.g., `ButtonHTMLAttributes`). + +### Styling +- Use Tailwind utility classes as the primary styling method. +- Use the `cn()` utility from `@/lib/utils` for conditional classes. +- Never use inline styles except for truly dynamic values. +- Never use CSS modules or styled-components. + +### Stories +- Every component MUST have a Storybook story file. +- Include at minimum: a `Default` story, and stories for each significant variant/state. +- Use Storybook Controls for interactive prop exploration. +- Use `tags: ['autodocs']` for automatic documentation generation. + +### Accessibility +- All interactive components must be keyboard-navigable. +- Use semantic HTML elements (` + ) + }, +) + +Button.displayName = 'Button' diff --git a/src/components/ui/Button/index.ts b/src/components/ui/Button/index.ts new file mode 100644 index 0000000..d7f8c69 --- /dev/null +++ b/src/components/ui/Button/index.ts @@ -0,0 +1,2 @@ +export { Button } from './Button' +export type { ButtonProps } from './Button' diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..d32b0fe --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..b5d9579 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './styles/global.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..571c383 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,15 @@ +@import "tailwindcss"; +@import "../tokens/tokens.css"; + +@layer base { + html { + font-size: 15px; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + line-height: 1.5; + background-color: var(--color-bg); + color: var(--color-text); + } +} diff --git a/src/tokens/tokens.css b/src/tokens/tokens.css new file mode 100644 index 0000000..768e0cf --- /dev/null +++ b/src/tokens/tokens.css @@ -0,0 +1,37 @@ +@theme { + /* Surface colors */ + --color-bg: #f5f6f8; + --color-surface: #ffffff; + --color-border: #e2e5ea; + + /* Text colors */ + --color-text: #1a1d23; + --color-text-secondary: #5f6672; + + /* Primary */ + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-primary-light: #eff4ff; + + /* Semantic: Success */ + --color-success: #16a34a; + --color-success-bg: #dcfce7; + + /* Semantic: Warning */ + --color-warning: #d97706; + --color-warning-bg: #fef9c3; + + /* Semantic: Error */ + --color-error: #dc2626; + --color-error-bg: #fee2e2; + + /* Radius */ + --radius-sm: 4px; + --radius-default: 6px; + --radius-lg: 10px; + --radius-full: 9999px; + + /* Shadows */ + --shadow-default: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..7a94817 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + + /* Path alias */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..bc011ef --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,41 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; +const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + +// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': '/src' + } + }, + test: { + projects: [{ + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + storybookTest({ + configDir: path.join(dirname, '.storybook') + })], + test: { + name: 'storybook', + browser: { + enabled: true, + headless: true, + provider: playwright({}), + instances: [{ + browser: 'chromium' + }] + } + } + }] + } +}); \ No newline at end of file diff --git a/vitest.shims.d.ts b/vitest.shims.d.ts new file mode 100644 index 0000000..7782f28 --- /dev/null +++ b/vitest.shims.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file