git:20260923.cf348bd to git:20261006.39320f8

26 added, 336 removed. Audit A to A.

---
name: design-system
description: >
Build a new design system (tokens, variants, theming). Use when
"create a design system" or "component library from scratch". Drifted
existing system → housekeep-design. Plan-only unification →
plan-uiux-unification.
license: MIT
---
# Design System Skill
**Degree of freedom: MIXED.** Token taxonomy and variants `[HIGH freedom]`;
existing `ui/` / token inventory `[LOW freedom — run exactly]`.
Build scalable, maintainable design systems with consistent tokens, variants, and documentation.
## How to reason
1. **Inventory** — `components/ui`, tokens, `cva`, shadcn/Radix
2. **Tokenize** — semantic tokens, not raw hex in components
3. **Variant** — `cva` covers real states (default/destructive/disabled)
4. **Document** — JSDoc, keyboard/focus, dark mode
## Worked example
> **Inventory:** shadcn `Button` exists; colors are raw `#3B82F6` in three pages; no `--primary`.
> **Tokenize:** `--primary` / `--primary-foreground` in `globals.css`; Tailwind maps `hsl(var(--primary))`.
> **Variant:** extend existing `buttonVariants` — do not add `components/Button2.tsx`.
> **Document:** JSDoc lists variants/sizes; focus ring + dark `.dark` overrides.
## Self-critique before reporting
- **Did not fork** — existing primitives were extended, not recreated
- **Semantic tokens** — components reference tokens, not primitives or hex
- **A11y + dark** — focus, disabled, and dark variants exist
- **Right owner** — drifted existing system → `housekeep-design`; plan-only unification → `plan-uiux-unification`
## Check existing first [LOW freedom — run exactly]
**Before creating ANY design system components, verify:**
1. **Check for existing design system:**
```bash
ls -la src/components/ui/
cat package.json | grep -i "shadcn\|radix\|headless"
cat components.json 2>/dev/null # shadcn config
```
2. **Check for existing tokens:**
```bash
cat tailwind.config.* | head -100
cat src/styles/globals.css | head -50
rg "var\(--" --type css | head -20
```
3. **Check for existing patterns:**
```bash
rg "cva\(|variants:" --type ts --type tsx | head -10
rg "cn\(|clsx\(|twMerge" --type tsx | head -5
```
**Why:** Don't recreate existing primitives. Extend and enhance what exists.
## Design Tokens [HIGH freedom]
Face and ground are choices, not defaults: Inter/Roboto/Arial, a cream/off-white page, and a purple-gradient primary are the fallbacks to name and avoid. Add `--font-sans` / `--font-display` tokens for the faces you pick.
### CSS Custom Properties
+ - Semantic HSL triplets on `:root` (`--background`, `--foreground`, `--primary`, `--muted`, `--destructive`, `--border`, `--ring`, …); `.dark` overrides the same names
+ - Spacing (`--spacing-xs…xl`), radius (`--radius` ± 4px), shadows (`--shadow-sm…lg`), durations and easings as tokens too
```css
- /* globals.css */
:root {
- /* Colors - Semantic */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
- --card: 0 0% 100%;
- --card-foreground: 222.2 84% 4.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 210 40% 98%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --ring: 221.2 83.2% 53.3%;
-
- /* Spacing */
- --spacing-xs: 0.25rem;
- --spacing-sm: 0.5rem;
- --spacing-md: 1rem;
- --spacing-lg: 1.5rem;
- --spacing-xl: 2rem;
-
- /* Border Radius */
--radius: 0.5rem;
- --radius-sm: calc(var(--radius) - 4px);
- --radius-lg: calc(var(--radius) + 4px);
-
- /* Shadows */
- --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
- --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
-
- /* Animation */
- --duration-fast: 150ms;
- --duration-normal: 200ms;
- --duration-slow: 300ms;
- --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
- --ease-in: cubic-bezier(0.4, 0, 1, 1);
- --ease-out: cubic-bezier(0, 0, 0.2, 1);
- --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
-
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
- /* ... dark mode overrides */
}
```
+ Full token set: [references/tokens.md](references/tokens.md) §CSS Custom Properties.
### Tailwind Config Integration
- ```ts
- // tailwind.config.ts
- import type { Config } from 'tailwindcss'
+ - `darkMode: 'class'`; every color is `hsl(var(--token))` with a `DEFAULT` / `foreground` pair
+ - `borderRadius`, `boxShadow`, `transitionDuration` read the CSS variables, never literals
- const config: Config = {
- content: ['./src/**/*.{ts,tsx}'],
- darkMode: 'class',
- theme: {
- extend: {
- colors: {
- border: 'hsl(var(--border))',
- input: 'hsl(var(--input))',
- ring: 'hsl(var(--ring))',
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
- primary: {
- DEFAULT: 'hsl(var(--primary))',
- foreground: 'hsl(var(--primary-foreground))',
- },
- secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))',
- },
- destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))',
- },
- muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))',
- },
- accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))',
- },
- },
- borderRadius: {
- lg: 'var(--radius-lg)',
- md: 'var(--radius)',
- sm: 'var(--radius-sm)',
- },
- boxShadow: {
- sm: 'var(--shadow-sm)',
- DEFAULT: 'var(--shadow)',
- md: 'var(--shadow-md)',
- lg: 'var(--shadow-lg)',
- },
- transitionDuration: {
- fast: 'var(--duration-fast)',
- normal: 'var(--duration-normal)',
- slow: 'var(--duration-slow)',
- },
- },
- },
- }
+ Full config: [references/tokens.md](references/tokens.md) §Tailwind Config Integration.
- export default config
- ```
+ ## Component Variants with CVA [HIGH freedom]
- ## Component Variants with CVA
+ - `cva()` base string carries focus-visible ring and `disabled:` styles; `variants.variant` (default/destructive/outline/secondary/ghost/link) and `variants.size` (default/sm/lg/icon) with `defaultVariants`
+ - Props extend the native element attributes plus `VariantProps<typeof buttonVariants>`; `forwardRef` and `asChild` via `Slot`
+ - Export both the component and its `*Variants` so consumers can reuse the classes
```tsx
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
```
- ```tsx
- // components/ui/button.tsx
- import { cva, type VariantProps } from 'class-variance-authority'
- import { forwardRef } from 'react'
- import { cn } from '@/lib/utils'
-
- const buttonVariants = cva(
- // Base styles
- 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
- {
- variants: {
- variant: {
- default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
- destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
- outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
- secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
- link: 'text-primary underline-offset-4 hover:underline',
- },
- size: {
- default: 'h-9 px-4 py-2',
- sm: 'h-8 rounded-md px-3 text-xs',
- lg: 'h-10 rounded-md px-8',
- icon: 'h-9 w-9',
- },
- },
- defaultVariants: {
- variant: 'default',
- size: 'default',
- },
- }
- )
-
- export interface ButtonProps
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
- VariantProps<typeof buttonVariants> {
- asChild?: boolean
- }
-
- const Button = forwardRef<HTMLButtonElement, ButtonProps>(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : 'button'
- return (
- <Comp
- className={cn(buttonVariants({ variant, size, className }))}
- ref={ref}
- {...props}
- />
- )
- }
- )
- Button.displayName = 'Button'
-
- export { Button, buttonVariants }
- ```
-
- ## Compound Components Pattern
-
- ```tsx
- // components/ui/card.tsx
- import { cn } from '@/lib/utils'
- import { forwardRef } from 'react'
-
- const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
- ({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn(
- 'rounded-xl border bg-card text-card-foreground shadow',
- className
- )}
- {...props}
- />
- )
- )
- Card.displayName = 'Card'
-
- const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
- ({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn('flex flex-col space-y-1.5 p-6', className)}
- {...props}
- />
- )
- )
- CardHeader.displayName = 'CardHeader'
-
- const CardTitle = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
- ({ className, ...props }, ref) => (
- <h3
- ref={ref}
- className={cn('font-semibold leading-none tracking-tight', className)}
- {...props}
- />
- )
- )
- CardTitle.displayName = 'CardTitle'
-
- const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
- ({ className, ...props }, ref) => (
- <p
- ref={ref}
- className={cn('text-sm text-muted-foreground', className)}
- {...props}
- />
- )
- )
- CardDescription.displayName = 'CardDescription'
-
- const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
- ({ className, ...props }, ref) => (
- <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
- )
- )
- CardContent.displayName = 'CardContent'
-
- const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
- ({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn('flex items-center p-6 pt-0', className)}
- {...props}
- />
- )
- )
- CardFooter.displayName = 'CardFooter'
-
- export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
- ```
-
- ## Accessible Components with Radix
+ Full `button.tsx`: [references/components.md](references/components.md) §Button with CVA.
- ```tsx
- // components/ui/dialog.tsx
- 'use client'
+ ## Compound Components Pattern [HIGH freedom]
- import * as DialogPrimitive from '@radix-ui/react-dialog'
- import { X } from 'lucide-react'
- import { cn } from '@/lib/utils'
+ - One file exports `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`
+ - Each is a `forwardRef` wrapper over the native element with `cn(defaults, className)` and a `displayName`
+ - Tokens only: `bg-card text-card-foreground`, `text-muted-foreground`
- const Dialog = DialogPrimitive.Root
- const DialogTrigger = DialogPrimitive.Trigger
- const DialogPortal = DialogPrimitive.Portal
- const DialogClose = DialogPrimitive.Close
+ Full `card.tsx`: [references/components.md](references/components.md) §Compound Card.
- const DialogOverlay = forwardRef<
- React.ElementRef<typeof DialogPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
- >(({ className, ...props }, ref) => (
- <DialogPrimitive.Overlay
- ref={ref}
- className={cn(
- 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
- className
- )}
- {...props}
- />
- ))
+ ## Accessible Components with Radix [HIGH freedom]
- const DialogContent = forwardRef<
- React.ElementRef<typeof DialogPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
- >(({ className, children, ...props }, ref) => (
- <DialogPortal>
- <DialogOverlay />
- <DialogPrimitive.Content
- ref={ref}
- className={cn(
- 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
- className
- )}
- {...props}
- >
- {children}
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
- <X className="h-4 w-4" />
- <span className="sr-only">Close</span>
- </DialogPrimitive.Close>
- </DialogPrimitive.Content>
- </DialogPortal>
- ))
+ - Re-export `Root`, `Trigger`, `Portal`, `Close` from `@radix-ui/react-dialog`; style `Overlay` and `Content` with `forwardRef`
+ - `data-[state=open]` / `data-[state=closed]` drive enter/exit animation classes
+ - A visible close button inside `Content` with an `sr-only` label
- export { Dialog, DialogTrigger, DialogContent, DialogClose }
- ```
+ Full `dialog.tsx`: [references/components.md](references/components.md) §Accessible Dialog with Radix.
- ## Component Documentation Pattern
+ ## Component Documentation Pattern [HIGH freedom]
- ```tsx
- // components/ui/button.stories.tsx (or docs)
- /**
- * Button Component
- *
- * A versatile button component with multiple variants and sizes.
- *
- * @example
- * ```tsx
- * <Button variant="default" size="md">Click me</Button>
- * <Button variant="outline" size="sm">Small</Button>
- * <Button variant="destructive" disabled>Disabled</Button>
- * ```
- *
- * ## Variants
- * - `default` - Primary action button
- * - `secondary` - Secondary actions
- * - `destructive` - Dangerous actions (delete, remove)
- * - `outline` - Less prominent actions
- * - `ghost` - Minimal visual weight
- * - `link` - Styled as a link
- *
- * ## Sizes
- * - `sm` - 32px height
- * - `default` - 36px height
- * - `lg` - 40px height
- * - `icon` - 36x36px square for icon-only buttons
- *
- * ## Accessibility
- * - Uses native `<button>` element
- * - Supports `disabled` attribute
- * - Focus ring visible on keyboard navigation
- * - Works with `asChild` prop for custom elements
- */
- ```
+ JSDoc block per component: one-line purpose, `@example` usage, then `## Variants`, `## Sizes` (with pixel heights), and `## Accessibility` (native element, `disabled`, focus ring, `asChild`).
+ Template: [references/components.md](references/components.md) §Component Documentation Pattern.
## File Structure
- ```
- src/
- ├── components/
- │ └── ui/
- │ ├── button.tsx
- │ ├── card.tsx
- │ ├── dialog.tsx
- │ ├── input.tsx
- │ ├── label.tsx
- │ ├── select.tsx
- │ ├── textarea.tsx
- │ ├── toast.tsx
- │ └── index.ts # Barrel export
- ├── lib/
- │ └── utils.ts # cn() helper
- ├── styles/
- │ └── globals.css # CSS tokens
- └── tailwind.config.ts # Theme config
- ```
+ `src/components/ui/*.tsx` with an `index.ts` barrel, `src/lib/utils.ts` for `cn()`, `src/styles/globals.css` for tokens, `tailwind.config.ts` for the theme.
+ Tree: [references/components.md](references/components.md) §File Structure.
## Validation [LOW freedom — do not skip]
After creating design system components:
1. **Consistency** → All components use same tokens
2. **Variants** → Cover all needed use cases
3. **Accessibility** → Keyboard nav, ARIA, focus states
4. **Dark mode** → All components work in dark mode
5. **Responsive** → Mobile-friendly by default
6. **Documentation** → JSDoc comments, usage examples
7. **Type safety** → Full TypeScript support with VariantProps