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

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