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 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 14:30:41 +10:00
commit dbf6a5870d
28 changed files with 7806 additions and 0 deletions

51
.claude/settings.json Normal file
View File

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

27
.gitignore vendored Normal file
View File

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

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}

17
.storybook/main.ts Normal file
View File

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

22
.storybook/preview.tsx Normal file
View File

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

112
ARCHITECTURE.md Normal file
View File

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

150
CLAUDE.md Normal file
View File

@@ -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<HTMLButtonElement>`).
### 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>`, `<nav>`, `<main>`, not `<div onClick>`).
- Include `aria-` attributes where needed.
- Storybook a11y addon is configured — check for violations.
---
## Code Patterns
### Conditional Classes
```tsx
import { cn } from '@/lib/utils'
<button className={cn(
'px-4 py-2 rounded-default font-medium',
variant === 'primary' && 'bg-primary text-white hover:bg-primary-hover',
variant === 'secondary' && 'bg-surface border border-border text-text',
disabled && 'opacity-50 cursor-not-allowed',
)}>
```
### Barrel Exports
Every component directory has an `index.ts`:
```ts
export { Button } from './Button'
export type { ButtonProps } from './Button'
```
### Path Alias
Use `@/` for imports from `src/`:
```ts
import { cn } from '@/lib/utils'
```
---
## MCP Integrations
- **Penpot MCP** — reads design specs from the self-hosted Penpot instance. Requires Penpot open in browser with the MCP plugin active, and `npx @penpot/mcp@stable` running locally.
- **Storybook MCP** — runs at `localhost:6006/mcp` alongside the Storybook dev server. Provides component docs, story generation, and a11y testing.
### Design-to-Code Workflow
1. Use Penpot MCP `high_level_overview` to understand the design file
2. Use `execute_code` with `findShapes()` to inspect specific components
3. Extract CSS with `penpot.generateStyle()`
4. Build React component using design tokens from `src/tokens/tokens.css`
5. Verify in Storybook that the component matches the Penpot design
---
## Reference Project
The existing synthesiser at `../SDC-Synthesiser/` contains the UI patterns, design tokens, and component designs this system is based on. Consult it when building components that map to the existing interface.
---
## Useful Subagents and Tools
- **Explore agent** — for codebase research spanning multiple files
- **webapp-testing skill** — for browser-based UI testing via Playwright
- **WebSearch/WebFetch** — for checking latest library documentation before making dependency decisions
- **Storybook MCP** — for AI-assisted story generation and component verification

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";
import eslintConfigPrettier from "eslint-config-prettier";
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([globalIgnores(['dist']), {
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
}, ...storybook.configs["flat/recommended"], eslintConfigPrettier])

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>sdc-frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6786
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "sdc-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
"clsx": "^2.1.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.2.1",
"@eslint/js": "^10.0.1",
"@storybook/addon-a11y": "^10.4.0",
"@storybook/addon-docs": "^10.4.0",
"@storybook/addon-mcp": "^0.6.0",
"@storybook/addon-vitest": "^10.4.0",
"@storybook/react-vite": "^10.4.0",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/browser-playwright": "^4.1.6",
"@vitest/coverage-v8": "^4.1.6",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-storybook": "^10.4.0",
"globals": "^17.6.0",
"playwright": "^1.60.0",
"prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.8.0",
"storybook": "^10.4.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
"vitest": "^4.1.6"
}
}

177
plans/project-setup.md Normal file
View File

@@ -0,0 +1,177 @@
# SDC-Frontend Project Setup Plan
## Context
The existing Research Synthesiser at `../SDC-Synthesiser/` is a vanilla HTML/CSS/JS app. This new project creates a React-based design system and component library that will eventually become the full frontend replacement. The design system is driven by Penpot (self-hosted at `192.168.50.211:9001`) via MCP, with Storybook for component development and documentation.
The project carries forward the same working principles (plan before building, evaluate before committing, ask don't assume, architecture governance) and the existing design tokens from the synthesiser's CSS.
---
## Tech Stack
| Tool | Version | Notes |
|---|---|---|
| React | 19.x | Function components, hooks only |
| Vite | latest | Dev server + build |
| TypeScript | strict | All components have typed props |
| Tailwind CSS | v4.3+ | CSS-first `@theme` config — no JS config file |
| Storybook | 10.x | Component dev, docs, MCP addon |
| ESLint + Prettier | latest | With `prettier-plugin-tailwindcss` for class sorting |
| clsx + tailwind-merge | latest | Combined as `cn()` utility |
---
## Steps
### 1. Scaffold Vite project
```bash
cd /home/richie/Nextcloud/Projects/Coding/
npm create vite@latest SDC-Frontend -- --template react-ts
cd SDC-Frontend
npm install
```
### 2. Install Tailwind CSS v4
```bash
npm install tailwindcss @tailwindcss/vite
```
Update `vite.config.ts` to add the Tailwind plugin alongside React.
### 3. Create design tokens and global styles
- `src/tokens/tokens.css``@theme` block with all design tokens ported from the synthesiser's CSS variables
- `src/styles/global.css` — imports Tailwind and tokens, sets base typography
- Update `src/main.tsx` to import `global.css`
- Delete Vite boilerplate (`App.css`, default `App.tsx` content, `src/assets/react.svg`)
### 4. Install utility dependencies
```bash
npm install clsx tailwind-merge
```
Create `src/lib/utils.ts` with `cn()` helper.
### 5. Configure path alias
Set up `@/``src/` in both `tsconfig.app.json` and `vite.config.ts` for clean imports.
### 6. Install and configure Storybook 10
```bash
npx storybook@latest init
npm install @storybook/addon-mcp
```
- Update `.storybook/main.ts` to add MCP addon
- Update `.storybook/preview.ts` to import `global.css` (so Tailwind works in stories)
### 7. Install and configure ESLint + Prettier
```bash
npm install -D prettier prettier-plugin-tailwindcss eslint-config-prettier
```
- Create `.prettierrc` with Tailwind plugin
- Update ESLint config to extend `prettier`
### 8. Create initial Button component
Validates the full pipeline: tokens → Tailwind → TypeScript → Storybook.
- `src/components/ui/Button/Button.tsx` — primary/secondary/danger variants, sm/md/lg sizes
- `src/components/ui/Button/Button.stories.tsx` — stories for each variant + disabled state
- `src/components/ui/Button/index.ts` — barrel export
### 9. Write CLAUDE.md
Carry over the five working principles from SDC-Synthesiser, adapted for React/TS/Tailwind/Storybook. Add:
- Component conventions (folder structure, typed props, forwardRef, stories required)
- Design token rules (single source of truth in `tokens.css`, never hardcode)
- Styling rules (Tailwind utilities via `cn()`, no inline styles, no CSS modules)
- Accessibility requirements (semantic HTML, keyboard nav, ARIA)
- MCP integrations (Penpot, Storybook)
- Reference to existing synthesiser
### 10. Write initial ARCHITECTURE.md
Lightweight living document covering:
- Token pipeline (Penpot → tokens.css → Tailwind → components)
- Component taxonomy (ui/composite/layout)
- Styling approach
- Storybook conventions
### 11. Create .claude/settings.json
**MCP servers:**
- `storybook``http://localhost:6006/mcp` (available when Storybook is running)
- `penpot``http://localhost:4401/mcp` (Penpot MCP server, runs via `npx @penpot/mcp@stable`)
Note: The Penpot UI is at `http://192.168.50.211:9001/`. The MCP server runs locally on the dev machine and connects to Penpot via the browser plugin.
**Permissions:** Pre-allow npm scripts, TypeScript checks, git operations, WebSearch, and WebFetch for documentation domains.
### 12. Create plans/ directory
Add this plan as the first entry.
### 13. Git init + initial commit
- `git init`
- Verify `.gitignore` covers `node_modules/`, `dist/`, `storybook-static/`, `.claude/settings.local.json`
- Stage all files, commit with descriptive message
- User provides Gitea remote URL to push
### 14. Verify end-to-end
1. `npm run dev` — Vite serves without errors
2. `npm run storybook` — Storybook loads, Button stories render with correct token colours
3. `npx tsc --noEmit` — TypeScript compiles clean
4. `npm run build` — produces `dist/` output
5. Storybook Controls work interactively on Button
6. Autodocs generate for Button
---
## Files created/modified
| File | Purpose |
|---|---|
| `vite.config.ts` | Add Tailwind plugin + path alias |
| `tsconfig.app.json` | Add `@/` path alias |
| `src/tokens/tokens.css` | Design tokens as `@theme` block |
| `src/styles/global.css` | Tailwind imports + base styles |
| `src/main.tsx` | Updated import path |
| `src/App.tsx` | Cleaned boilerplate |
| `src/lib/utils.ts` | `cn()` utility |
| `src/components/ui/Button/Button.tsx` | First component |
| `src/components/ui/Button/Button.stories.tsx` | First stories |
| `src/components/ui/Button/index.ts` | Barrel export |
| `.storybook/main.ts` | MCP addon added |
| `.storybook/preview.ts` | Global CSS import |
| `.prettierrc` | Tailwind class sorting |
| `CLAUDE.md` | Project principles + conventions |
| `ARCHITECTURE.md` | Living architecture doc |
| `.claude/settings.json` | MCP servers + permissions |
| `plans/project-setup.md` | This plan |
---
## Penpot MCP Setup (user action required)
After the project is scaffolded, the user needs to:
1. Install the Penpot MCP plugin inside their Penpot instance at `192.168.50.211:9001`
2. Run `npx @penpot/mcp@stable` on their dev machine
3. Open Penpot in the browser with the MCP plugin active
4. Claude Code can then use the `penpot` MCP tools
---
## Open items
- **Gitea remote URL** — user will provide when ready to push
- **Penpot MCP plugin installation** — user needs to install in their Penpot instance

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

16
src/App.tsx Normal file
View File

@@ -0,0 +1,16 @@
function App() {
return (
<div className="min-h-screen bg-bg text-text">
<header className="bg-surface border-b border-border px-6 py-3">
<h1 className="text-lg font-semibold">SDC Design System</h1>
</header>
<main className="p-6">
<p className="text-text-secondary">
Component library for the Research Synthesiser.
</p>
</main>
</div>
)
}
export default App

View File

@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { Button } from './Button'
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: { control: 'boolean' },
children: { control: 'text' },
},
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: { children: 'Primary Button', variant: 'primary' },
}
export const Secondary: Story = {
args: { children: 'Secondary Button', variant: 'secondary' },
}
export const Danger: Story = {
args: { children: 'Delete', variant: 'danger' },
}
export const Small: Story = {
args: { children: 'Small', size: 'sm' },
}
export const Large: Story = {
args: { children: 'Large', size: 'lg' },
}
export const Disabled: Story = {
args: { children: 'Disabled', disabled: true },
}

View File

@@ -0,0 +1,34 @@
import { forwardRef, type ButtonHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', children, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center font-medium rounded-default transition-colors',
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary',
'disabled:opacity-50 disabled:cursor-not-allowed',
variant === 'primary' && 'bg-primary text-white hover:bg-primary-hover',
variant === 'secondary' && 'bg-surface text-text border border-border hover:bg-bg',
variant === 'danger' && 'bg-error text-white hover:bg-red-700',
size === 'sm' && 'px-3 py-1.5 text-sm',
size === 'md' && 'px-4 py-2 text-base',
size === 'lg' && 'px-6 py-3 text-lg',
className,
)}
{...props}
>
{children}
</button>
)
},
)
Button.displayName = 'Button'

View File

@@ -0,0 +1,2 @@
export { Button } from './Button'
export type { ButtonProps } from './Button'

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

10
src/main.tsx Normal file
View File

@@ -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(
<StrictMode>
<App />
</StrictMode>,
)

15
src/styles/global.css Normal file
View File

@@ -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);
}
}

37
src/tokens/tokens.css Normal file
View File

@@ -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);
}

32
tsconfig.app.json Normal file
View File

@@ -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"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@@ -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"]
}

41
vite.config.ts Normal file
View File

@@ -0,0 +1,41 @@
/// <reference types="vitest/config" />
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'
}]
}
}
}]
}
});

1
vitest.shims.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="@vitest/browser-playwright" />