# Greyhaven Design System -- Claude Skill

> **Auto-generated** by `scripts/generate-skill.ts` -- DO NOT EDIT by hand.
> Re-generate: `pnpm skill:build`
>
> **Components**: 38 | **Style**: shadcn/ui "new-york"
> **Stack**: React 19, Radix UI, Tailwind CSS v4, CVA, tailwind-merge, clsx, Lucide icons
> **Framework-agnostic**: No Next.js imports. Works with Vite, Remix, Astro, CRA, or any React setup.

This skill gives you full context to generate pixel-perfect, on-brand UI using the Greyhaven Design System. Every component lives in `components/ui/`. Use semantic tokens, never raw colors. Follow the patterns exactly. **ALWAYS use TypeScript (.tsx/.ts) — never plain JavaScript.**

---

## Design Philosophy

- **TypeScript only**: All code MUST be written in TypeScript (`.tsx` / `.ts`). Never generate plain JavaScript (`.jsx` / `.js`).
- **Minimal and restrained**: Off-white + off-black + warm greys. One single accent color (orange). No gradients, no decorative color, no multiple accent hues.
- **Typography-driven**: Source Serif 4/Pro (serif) for headings and body content. Aspekta (sans, self-hosted) for UI labels, buttons, navigation, and form elements.
- **Calm, professional aesthetic**: Tight border-radii, subtle shadows, generous whitespace.
- **Accessibility-first**: Built on Radix UI primitives for keyboard navigation, focus management, screen reader support. Visible focus rings, disabled states, ARIA attributes.
- **Dark mode native**: Thoughtful dark theme using inverted warm greys. Orange accent persists across both modes. Toggled via `.dark` class.
- **Framework-agnostic**: Pure React + Radix + Tailwind. No Next.js, no framework-specific imports.

---

## Font Setup

This design system uses two typefaces:

| Role | Font | Usage |
|------|------|-------|
| **Sans (UI)** | Aspekta (self-hosted) | Buttons, nav, labels, forms, metadata |
| **Serif (Content)** | Source Serif 4/Pro | Headings, body text, reading content |

### Aspekta (required)

Aspekta font files live in `public/fonts/`. Add `@font-face` declarations to your global CSS:

```css
/* Minimum set (covers font-weight 400-700) */
@font-face { font-family: 'Aspekta'; font-weight: 400; font-display: swap; src: url('/fonts/Aspekta-400.woff2') format('woff2'); }
@font-face { font-family: 'Aspekta'; font-weight: 500; font-display: swap; src: url('/fonts/Aspekta-500.woff2') format('woff2'); }
@font-face { font-family: 'Aspekta'; font-weight: 600; font-display: swap; src: url('/fonts/Aspekta-600.woff2') format('woff2'); }
@font-face { font-family: 'Aspekta'; font-weight: 700; font-display: swap; src: url('/fonts/Aspekta-700.woff2') format('woff2'); }

/* Or import all weights: */
@import url('/fonts/font-face.css');
```

### Font stack CSS variables

```css
--font-sans: 'Aspekta', ui-sans-serif, system-ui, sans-serif;
--font-serif: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;
```

### Tailwind usage

- `font-sans` — Aspekta (UI elements)
- `font-serif` — Source Serif (content)

Install fonts via: `./skill/install.sh /path/to/your/project`

---

## Token Quick Reference

Source of truth: `tokens/*.json` (W3C DTCG format).

### Color

| Token | Value | Description |
|-------|-------|-------------|
| `color.primitive.off-white` | `#F9F9F7` | Primary light surface — cards, elevated areas |
| `color.primitive.off-black` | `#161614` | Primary dark — foreground text, dark mode background |
| `color.primitive.orange` | `#D95E2A` | Only accent color — used sparingly for primary actions and emphasis |
| `color.primitive.destructive-red` | `#B43232` | Error/danger states |
| `color.primitive.grey.1` | `#F0F0EC` | 5% — Subtle backgrounds, secondary, muted |
| `color.primitive.grey.2` | `#DDDDD7` | 10% — Accent hover, light borders |
| `color.primitive.grey.3` | `#C4C4BD` | 20% — Border, input |
| `color.primitive.grey.4` | `#A6A69F` | 50% — Mid-tone |
| `color.primitive.grey.5` | `#7F7F79` | 60% — Mid-dark |
| `color.primitive.grey.7` | `#575753` | 70% — Secondary foreground, muted foreground |
| `color.primitive.grey.8` | `#2F2F2C` | 80% — Dark mode card, dark surfaces |
| `color.semantic.background` | `{color.primitive.grey.1}` | Page background |
| `color.semantic.foreground` | `{color.primitive.off-black}` | Primary text |
| `color.semantic.card` | `{color.primitive.off-white}` | Card/elevated surface background |
| `color.semantic.card-foreground` | `{color.primitive.off-black}` | Card text |
| `color.semantic.popover` | `{color.primitive.off-white}` | Popover background |
| `color.semantic.popover-foreground` | `{color.primitive.off-black}` | Popover text |
| `color.semantic.primary` | `{color.primitive.orange}` | Primary accent — buttons, links, focus rings |
| `color.semantic.primary-foreground` | `{color.primitive.off-white}` | Text on primary accent |
| `color.semantic.secondary` | `{color.primitive.grey.1}` | Secondary button/surface |
| `color.semantic.secondary-foreground` | `{color.primitive.grey.8}` | Text on secondary surface |
| `color.semantic.muted` | `{color.primitive.grey.1}` | Muted/subdued background |
| `color.semantic.muted-foreground` | `{color.primitive.grey.7}` | Muted/subdued text |
| `color.semantic.accent` | `{color.primitive.grey.2}` | Subtle hover state |
| `color.semantic.accent-foreground` | `{color.primitive.off-black}` | Text on accent hover |
| `color.semantic.destructive` | `{color.primitive.destructive-red}` | Destructive/error actions |
| `color.semantic.destructive-foreground` | `{color.primitive.off-white}` | Text on destructive |
| `color.semantic.border` | `{color.primitive.grey.3}` | Default border |
| `color.semantic.input` | `{color.primitive.grey.3}` | Input border |
| `color.semantic.ring` | `{color.primitive.orange}` | Focus ring |
| `color.semantic.hero-bg` | `{color.primitive.grey.2}` | Hero banner background |
| `color.semantic.chart.1` | `{color.primitive.orange}` | Chart accent |
| `color.semantic.chart.2` | `{color.primitive.grey.7}` | Chart secondary |
| `color.semantic.chart.3` | `{color.primitive.grey.5}` | Chart tertiary |
| `color.semantic.chart.4` | `{color.primitive.grey.4}` | Chart quaternary |
| `color.semantic.chart.5` | `{color.primitive.grey.8}` | Chart quinary |
| `color.semantic.sidebar.background` | `{color.primitive.grey.1}` | Sidebar background |
| `color.semantic.sidebar.foreground` | `{color.primitive.off-black}` | Sidebar text |
| `color.semantic.sidebar.primary` | `{color.primitive.orange}` | Sidebar primary accent |
| `color.semantic.sidebar.primary-foreground` | `{color.primitive.off-white}` | Sidebar primary text |
| `color.semantic.sidebar.accent` | `{color.primitive.grey.3}` | Sidebar accent/hover |
| `color.semantic.sidebar.accent-foreground` | `{color.primitive.off-black}` | Sidebar accent text |
| `color.semantic.sidebar.border` | `{color.primitive.grey.3}` | Sidebar border |
| `color.semantic.sidebar.ring` | `{color.primitive.orange}` | Sidebar focus ring |
| `color.dark.background` | `{color.primitive.off-black}` | Dark page background |
| `color.dark.foreground` | `{color.primitive.off-white}` | Dark primary text |
| `color.dark.card` | `{color.primitive.grey.8}` | Dark card surface |
| `color.dark.card-foreground` | `{color.primitive.off-white}` | Dark card text |
| `color.dark.popover` | `{color.primitive.grey.8}` | Dark popover |
| `color.dark.popover-foreground` | `{color.primitive.off-white}` | Dark popover text |
| `color.dark.primary` | `{color.primitive.orange}` | Same orange in dark mode |
| `color.dark.primary-foreground` | `{color.primitive.off-white}` | Dark primary foreground |
| `color.dark.secondary` | `{color.primitive.grey.7}` | Dark secondary |
| `color.dark.secondary-foreground` | `{color.primitive.off-white}` | Dark secondary text |
| `color.dark.muted` | `{color.primitive.grey.8}` | Dark muted — grey.8 (distinct from grey.7 border so outlines on bg-muted surfaces remain visible) |
| `color.dark.muted-foreground` | `{color.primitive.grey.3}` | Dark muted text |
| `color.dark.accent` | `{color.primitive.grey.7}` | Dark accent/hover |
| `color.dark.accent-foreground` | `{color.primitive.off-white}` | Dark accent text |
| `color.dark.destructive` | `{color.primitive.destructive-red}` | Same destructive in dark mode |
| `color.dark.destructive-foreground` | `{color.primitive.off-white}` | Dark destructive text |
| `color.dark.border` | `{color.primitive.grey.7}` | Dark border |
| `color.dark.input` | `{color.primitive.grey.7}` | Dark input border |
| `color.dark.ring` | `{color.primitive.orange}` | Dark focus ring |
| `color.dark.hero-bg` | `{color.primitive.grey.8}` | Dark hero banner background |
| `color.dark.chart.1` | `{color.primitive.orange}` | Dark chart accent |
| `color.dark.chart.2` | `{color.primitive.grey.3}` | Dark chart secondary |
| `color.dark.chart.3` | `{color.primitive.grey.4}` | Dark chart tertiary |
| `color.dark.chart.4` | `{color.primitive.grey.5}` | Dark chart quaternary |
| `color.dark.chart.5` | `{color.primitive.grey.1}` | Dark chart quinary |
| `color.dark.sidebar.background` | `{color.primitive.grey.8}` | Dark sidebar background |
| `color.dark.sidebar.foreground` | `{color.primitive.off-white}` | Dark sidebar text |
| `color.dark.sidebar.primary` | `{color.primitive.orange}` | Dark sidebar primary |
| `color.dark.sidebar.primary-foreground` | `{color.primitive.off-white}` | Dark sidebar primary text |
| `color.dark.sidebar.accent` | `{color.primitive.grey.7}` | Dark sidebar accent |
| `color.dark.sidebar.accent-foreground` | `{color.primitive.off-white}` | Dark sidebar accent text |
| `color.dark.sidebar.border` | `{color.primitive.grey.7}` | Dark sidebar border |
| `color.dark.sidebar.ring` | `{color.primitive.orange}` | Dark sidebar ring |

### Typography

| Token | Value | Description |
|-------|-------|-------------|
| `typography.fontFamily.sans` | `["Aspekta","ui-sans-serif","system-ui","sans-serif"]` | UI labels, buttons, nav, forms — Aspekta self-hosted |
| `typography.fontFamily.serif` | `["Source Serif 4","Source Serif Pro","Georgia","serif"]` | Headings, body content, reading — Source Serif primary |
| `typography.fontFamily.mono` | `["ui-monospace","SFMono-Regular","Menlo","Monaco","Consolas","monospace"]` | Code blocks and monospaced content |
| `typography.fontSize.xs` | `0.75rem` | 12px — metadata, fine print |
| `typography.fontSize.sm` | `0.875rem` | 14px — captions, nav, labels, buttons |
| `typography.fontSize.base` | `1rem` | 16px — body text |
| `typography.fontSize.lg` | `1.125rem` | 18px — large body, subtitles |
| `typography.fontSize.xl` | `1.25rem` | 20px — H3 |
| `typography.fontSize.2xl` | `1.5rem` | 24px — H2 |
| `typography.fontSize.3xl` | `1.875rem` | 30px — large H2 |
| `typography.fontSize.4xl` | `2.25rem` | 36px — H1 |
| `typography.fontSize.5xl` | `3rem` | 48px — hero heading |
| `typography.fontWeight.normal` | `400` | Regular body text |
| `typography.fontWeight.medium` | `500` | H3, labels, nav items |
| `typography.fontWeight.semibold` | `600` | H1, H2, buttons |
| `typography.fontWeight.bold` | `700` | Strong emphasis |
| `typography.lineHeight.tight` | `1.25` | Headings |
| `typography.lineHeight.normal` | `1.5` | Default |
| `typography.lineHeight.relaxed` | `1.625` | Body content for readability |
| `typography.letterSpacing.tight` | `-0.025em` | Headings — tracking-tight |
| `typography.letterSpacing.normal` | `0em` | Body text |
| `typography.letterSpacing.wide` | `0.05em` | Uppercase labels |

### Spacing

| Token | Value | Description |
|-------|-------|-------------|
| `spacing.0` | `0` | None |
| `spacing.1` | `0.25rem` | 4px — tight gaps |
| `spacing.2` | `0.5rem` | 8px — card header gap, form description spacing |
| `spacing.3` | `0.75rem` | 12px |
| `spacing.4` | `1rem` | 16px — form field gap, button padding |
| `spacing.5` | `1.25rem` | 20px |
| `spacing.6` | `1.5rem` | 24px — card padding, card internal gap |
| `spacing.8` | `2rem` | 32px — section margin-bottom |
| `spacing.10` | `2.5rem` | 40px |
| `spacing.12` | `3rem` | 48px |
| `spacing.16` | `4rem` | 64px — major section padding (py-16) |
| `spacing.20` | `5rem` | 80px |
| `spacing.24` | `6rem` | 96px — hero padding |
| `spacing.0.5` | `0.125rem` | 2px — micro spacing |
| `spacing.1.5` | `0.375rem` | 6px |
| `spacing.component.card-padding` | `1.5rem` | Card internal padding (px-6) |
| `spacing.component.card-gap` | `1.5rem` | Gap between cards (gap-6) |
| `spacing.component.section-padding` | `2.5rem` | Vertical padding inside sections (py-10) |
| `spacing.component.form-gap` | `1rem` | Gap between form fields (gap-4) |
| `spacing.component.button-padding-x` | `1rem` | Button horizontal padding (px-4) |
| `spacing.component.navbar-height` | `4rem` | Navbar height (h-16) |

### Radii

| Token | Value | Description |
|-------|-------|-------------|
| `radii.base` | `0.375rem` | 6px — base radius |
| `radii.sm` | `calc(0.375rem - 2px)` | 4px — small variant |
| `radii.md` | `0.375rem` | 6px — medium (same as base) |
| `radii.lg` | `calc(0.375rem + 2px)` | 8px — large variant |
| `radii.xl` | `calc(0.375rem + 4px)` | 10px — extra large variant (cards) |
| `radii.full` | `9999px` | Fully round (pills, avatars) |

### Shadows

| Token | Value | Description |
|-------|-------|-------------|
| `shadow.xs` | `{"offsetX":"0","offsetY":"1px","blur":"2px","spread":"0","color":"rgba(22, 22, 20, 0.05)"}` | Subtle shadow for buttons, inputs |
| `shadow.sm` | `{"offsetX":"0","offsetY":"1px","blur":"3px","spread":"0","color":"rgba(22, 22, 20, 0.1)"}` | Small shadow for cards |
| `shadow.md` | `{"offsetX":"0","offsetY":"4px","blur":"6px","spread":"-1px","color":"rgba(22, 22, 20, 0.1)"}` | Medium shadow for dropdowns, popovers |
| `shadow.lg` | `{"offsetX":"0","offsetY":"10px","blur":"15px","spread":"-3px","color":"rgba(22, 22, 20, 0.1)"}` | Large shadow for dialogs, modals |

### Motion

| Token | Value | Description |
|-------|-------|-------------|
| `motion.duration.fast` | `150ms` | Quick transitions — tooltips, hover states |
| `motion.duration.normal` | `200ms` | Default transitions — most UI interactions |
| `motion.duration.slow` | `300ms` | Deliberate transitions — modals, drawers, accordions |
| `motion.easing.default` | `[0.4,0,0.2,1]` | Standard ease-in-out |
| `motion.easing.in` | `[0.4,0,1,1]` | Ease-in for exits |
| `motion.easing.out` | `[0,0,0.2,1]` | Ease-out for entrances |

---

## Component Catalog (38 components)

All components live in `components/ui/`. Import with `@/components/ui/<name>`.

### Primitives

#### Button
- **File**: `components/ui/button.tsx`
- **Exports**: `Button`, `buttonVariants`
- **Description**: Primary interactive element. 6 variants: default (orange), secondary, outline, ghost, link, destructive. Sizes: default (h-9), sm (h-8), lg (h-10), icon (size-9).
- **Props**: `variant?: "default" | "secondary" | "outline" | "ghost" | "link" | "destructive"; size?: "default" | "sm" | "lg" | "icon" | "icon-sm" | "icon-lg"; asChild?: boolean`
- **Example**:
```tsx
<Button variant="default" size="default">Click me</Button>
```

#### Badge
- **File**: `components/ui/badge.tsx`
- **Exports**: `Badge`, `badgeVariants`
- **Description**: Status indicator / tag. Variants: default, secondary, muted, outline, destructive, success, warning, info, tag, value, whatsapp, email, telegram, zulip, platform. Sizes: sm (dense data/tables), default (most uses), lg (hero-adjacent, near large type). NEVER override font-size or padding with className — pick a size variant instead. Anything below text-xs (12px) fails accessibility minimums.
- **Props**: `variant?: "default" | "secondary" | "muted" | "destructive" | "outline" | "success" | "warning" | "info" | "tag" | "value" | "whatsapp" | "email" | "telegram" | "zulip" | "platform"; size?: "sm" | "default" | "lg"; asChild?: boolean`
- **Example**:
```tsx
<Badge variant="success">Active</Badge>
<Badge variant="secondary" size="sm">3 items</Badge>
<Badge variant="default" size="lg">New feature</Badge>
```

#### Input
- **File**: `components/ui/input.tsx`
- **Exports**: `Input`
- **Description**: Text input field with focus ring, disabled, and aria-invalid states.
- **Props**: `All standard HTML input props`
- **Example**:
```tsx
<Input type="email" placeholder="you@example.com" />
```

#### Textarea
- **File**: `components/ui/textarea.tsx`
- **Exports**: `Textarea`
- **Description**: Multi-line text input.
- **Props**: `All standard HTML textarea props`
- **Example**:
```tsx
<Textarea placeholder="Write your message..." />
```

#### Label
- **File**: `components/ui/label.tsx`
- **Exports**: `Label`
- **Description**: Form label using Radix Label primitive.
- **Props**: `All standard HTML label props + Radix Label props`
- **Example**:
```tsx
<Label htmlFor="email">Email</Label>
```

#### Checkbox
- **File**: `components/ui/checkbox.tsx`
- **Exports**: `Checkbox`
- **Description**: Checkbox using Radix Checkbox primitive.
- **Props**: `checked?: boolean; onCheckedChange?: (checked: boolean) => void`
- **Example**:
```tsx
<Checkbox id="terms" />
```

#### Switch
- **File**: `components/ui/switch.tsx`
- **Exports**: `Switch`
- **Description**: Toggle switch using Radix Switch primitive.
- **Props**: `checked?: boolean; onCheckedChange?: (checked: boolean) => void`
- **Example**:
```tsx
<Switch id="dark-mode" />
```

#### Select
- **File**: `components/ui/select.tsx`
- **Exports**: `Select`, `SelectContent`, `SelectGroup`, `SelectItem`, `SelectLabel`, `SelectTrigger`, `SelectValue`
- **Description**: Dropdown select using Radix Select.
- **Props**: `value?: string; onValueChange?: (value: string) => void`
- **Example**:
```tsx
<Select><SelectTrigger><SelectValue placeholder="Choose..." /></SelectTrigger><SelectContent><SelectItem value="a">Option A</SelectItem></SelectContent></Select>
```

#### RadioGroup
- **File**: `components/ui/radio-group.tsx`
- **Exports**: `RadioGroup`, `RadioGroupItem`
- **Description**: Radio button group using Radix RadioGroup.
- **Props**: `value?: string; onValueChange?: (value: string) => void`
- **Example**:
```tsx
<RadioGroup defaultValue="a"><RadioGroupItem value="a" /><RadioGroupItem value="b" /></RadioGroup>
```

#### Toggle
- **File**: `components/ui/toggle.tsx`
- **Exports**: `Toggle`, `toggleVariants`
- **Description**: Toggle button. Variants: default, outline.
- **Props**: `variant?: "default" | "outline"; size?: "default" | "sm" | "lg"; pressed?: boolean`
- **Example**:
```tsx
<Toggle aria-label="Bold"><BoldIcon /></Toggle>
```

#### Code
- **File**: `components/ui/code.tsx`
- **Exports**: `Code`, `codeVariants`
- **Description**: Inline or block code snippet. Always use this instead of hand-rolling <code>/<pre> styling. Uses bg-muted + border-border so the outline stays visible in both light and dark modes. Block variant auto-wraps in <pre> for whitespace preservation and break-all for long commands.
- **Props**: `variant?: "inline" | "block"; language?: string (optional, for future syntax highlighting)`
- **Example**:
```tsx
<p>Install with <Code>pnpm install</Code>.</p>

<Code variant="block" language="bash">{`pnpm install
pnpm dev`}</Code>
```

### Layout

#### Card
- **File**: `components/ui/card.tsx`
- **Exports**: `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`
- **Description**: Container with header/content/footer slots. Off-white bg, rounded-xl, subtle shadow.
- **Props**: `Standard div props. Compose with CardHeader, CardTitle, CardDescription, CardContent, CardFooter sub-components.`
- **Example**:
```tsx
<Card><CardHeader><CardTitle>Title</CardTitle><CardDescription>Description</CardDescription></CardHeader><CardContent>Content</CardContent></Card>
```

#### Accordion
- **File**: `components/ui/accordion.tsx`
- **Exports**: `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`
- **Description**: Expandable sections using Radix Accordion.
- **Props**: `type: "single" | "multiple"; collapsible?: boolean`
- **Example**:
```tsx
<Accordion type="single" collapsible><AccordionItem value="item-1"><AccordionTrigger>Section 1</AccordionTrigger><AccordionContent>Content</AccordionContent></AccordionItem></Accordion>
```

#### Tabs
- **File**: `components/ui/tabs.tsx`
- **Exports**: `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`
- **Description**: Tab navigation using Radix Tabs. Pill-style triggers.
- **Props**: `value?: string; onValueChange?: (value: string) => void`
- **Example**:
```tsx
<Tabs defaultValue="tab1"><TabsList><TabsTrigger value="tab1">Tab 1</TabsTrigger></TabsList><TabsContent value="tab1">Content</TabsContent></Tabs>
```

#### Separator
- **File**: `components/ui/separator.tsx`
- **Exports**: `Separator`
- **Description**: Visual divider line. Horizontal or vertical.
- **Props**: `orientation?: "horizontal" | "vertical"; decorative?: boolean`
- **Example**:
```tsx
<Separator />
```

### Overlay

#### Dialog
- **File**: `components/ui/dialog.tsx`
- **Exports**: `Dialog`, `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogFooter`, `DialogClose`
- **Description**: Modal dialog using Radix Dialog.
- **Props**: `open?: boolean; onOpenChange?: (open: boolean) => void`
- **Example**:
```tsx
<Dialog><DialogTrigger asChild><Button>Open</Button></DialogTrigger><DialogContent><DialogHeader><DialogTitle>Title</DialogTitle></DialogHeader></DialogContent></Dialog>
```

#### AlertDialog
- **File**: `components/ui/alert-dialog.tsx`
- **Exports**: `AlertDialog`, `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogFooter`, `AlertDialogAction`, `AlertDialogCancel`
- **Description**: Confirmation dialog requiring user action.
- **Props**: `open?: boolean; onOpenChange?: (open: boolean) => void`
- **Example**:
```tsx
<AlertDialog><AlertDialogTrigger asChild><Button variant="destructive">Delete</Button></AlertDialogTrigger><AlertDialogContent>...</AlertDialogContent></AlertDialog>
```

#### Tooltip
- **File**: `components/ui/tooltip.tsx`
- **Exports**: `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`
- **Description**: Tooltip popup (0ms delay) using Radix Tooltip.
- **Props**: `Standard Radix Tooltip props`
- **Example**:
```tsx
<TooltipProvider><Tooltip><TooltipTrigger>Hover me</TooltipTrigger><TooltipContent>Tooltip text</TooltipContent></Tooltip></TooltipProvider>
```

#### Popover
- **File**: `components/ui/popover.tsx`
- **Exports**: `Popover`, `PopoverTrigger`, `PopoverContent`
- **Description**: Floating content panel using Radix Popover.
- **Props**: `open?: boolean; onOpenChange?: (open: boolean) => void`
- **Example**:
```tsx
<Popover><PopoverTrigger asChild><Button>Open</Button></PopoverTrigger><PopoverContent>Content</PopoverContent></Popover>
```

#### Drawer
- **File**: `components/ui/drawer.tsx`
- **Exports**: `Drawer`, `DrawerTrigger`, `DrawerContent`, `DrawerHeader`, `DrawerTitle`, `DrawerDescription`, `DrawerFooter`, `DrawerClose`
- **Description**: Bottom sheet drawer using Vaul.
- **Props**: `open?: boolean; onOpenChange?: (open: boolean) => void`
- **Example**:
```tsx
<Drawer><DrawerTrigger asChild><Button>Open</Button></DrawerTrigger><DrawerContent><DrawerHeader><DrawerTitle>Title</DrawerTitle></DrawerHeader></DrawerContent></Drawer>
```

### Navigation

#### Navbar
- **File**: `components/ui/navbar.tsx`
- **Exports**: `Navbar`, `NavbarLink`, `navbarVariants`
- **Description**: Top navigation bar. Fixed top, z-50, h-[65px]. Off-white bg (light) / off-black (dark). Font-semibold menu items. Hover: opacity-70 (no bg). Active links: orange (text-primary), full opacity. Variants: solid, transparent, minimal. Logo left, nav center, actions right. Mobile hamburger.
- **Props**: `variant?: "solid" | "transparent" | "minimal"; logo?: ReactNode; actions?: ReactNode. NavbarLink: active?: boolean`
- **Example**:
```tsx
<Navbar variant="solid" logo={<Logo size="sm" />}><NavbarLink href="/" active>Home</NavbarLink><NavbarLink href="/about">About</NavbarLink></Navbar>
```

#### Breadcrumb
- **File**: `components/ui/breadcrumb.tsx`
- **Exports**: `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis`
- **Description**: Breadcrumb navigation trail.
- **Props**: `Standard list composition`
- **Example**:
```tsx
<Breadcrumb><BreadcrumbList><BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbPage>Current</BreadcrumbPage></BreadcrumbItem></BreadcrumbList></Breadcrumb>
```

#### Pagination
- **File**: `components/ui/pagination.tsx`
- **Exports**: `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink`, `PaginationPrevious`, `PaginationNext`, `PaginationEllipsis`
- **Description**: Page navigation controls.
- **Props**: `Standard list composition with PaginationLink items`
- **Example**:
```tsx
<Pagination><PaginationContent><PaginationItem><PaginationPrevious href="#" /></PaginationItem><PaginationItem><PaginationLink href="#">1</PaginationLink></PaginationItem><PaginationItem><PaginationNext href="#" /></PaginationItem></PaginationContent></Pagination>
```

### Data

#### Table
- **File**: `components/ui/table.tsx`
- **Exports**: `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`, `TableCaption`, `TableFooter`
- **Description**: Data table with header, body, footer.
- **Props**: `Standard HTML table element composition`
- **Example**:
```tsx
<Table><TableHeader><TableRow><TableHead>Name</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell>John</TableCell></TableRow></TableBody></Table>
```

#### Progress
- **File**: `components/ui/progress.tsx`
- **Exports**: `Progress`
- **Description**: Progress bar using Radix Progress.
- **Props**: `value?: number (0-100)`
- **Example**:
```tsx
<Progress value={60} />
```

#### Avatar
- **File**: `components/ui/avatar.tsx`
- **Exports**: `Avatar`, `AvatarImage`, `AvatarFallback`
- **Description**: User avatar with image and fallback.
- **Props**: `Standard Radix Avatar composition`
- **Example**:
```tsx
<Avatar><AvatarImage src="/avatar.jpg" /><AvatarFallback>JD</AvatarFallback></Avatar>
```

#### Calendar
- **File**: `components/ui/calendar.tsx`
- **Exports**: `Calendar`
- **Description**: Date picker calendar using react-day-picker.
- **Props**: `mode?: "single" | "range" | "multiple"; selected?: Date; onSelect?: (date: Date) => void`
- **Example**:
```tsx
<Calendar mode="single" selected={date} onSelect={setDate} />
```

### Feedback

#### Alert
- **File**: `components/ui/alert.tsx`
- **Exports**: `Alert`, `AlertTitle`, `AlertDescription`
- **Description**: Inline alert message. Variants: default, destructive.
- **Props**: `variant?: "default" | "destructive"`
- **Example**:
```tsx
<Alert><AlertTitle>Heads up!</AlertTitle><AlertDescription>This is an alert.</AlertDescription></Alert>
```

#### Skeleton
- **File**: `components/ui/skeleton.tsx`
- **Exports**: `Skeleton`
- **Description**: Loading placeholder with pulse animation.
- **Props**: `Standard div props (set dimensions with className)`
- **Example**:
```tsx
<Skeleton className="h-4 w-[250px]" />
```

#### Spinner
- **File**: `components/ui/spinner.tsx`
- **Exports**: `Spinner`
- **Description**: Loading spinner (Loader2Icon with spin animation).
- **Props**: `Standard SVG icon props`
- **Example**:
```tsx
<Spinner />
```

#### Empty
- **File**: `components/ui/empty.tsx`
- **Exports**: `Empty`
- **Description**: Empty state placeholder with header/media/title/description.
- **Props**: `Standard composition with sub-components`
- **Example**:
```tsx
<Empty><EmptyTitle>No results</EmptyTitle><EmptyDescription>Try a different search</EmptyDescription></Empty>
```

### Form

#### Form
- **File**: `components/ui/form.tsx`
- **Exports**: `Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, `FormMessage`
- **Description**: Form wrapper using react-hook-form. Provides field-level validation and error display via Zod.
- **Props**: `Wraps react-hook-form useForm return value. FormField takes name + render prop.`
- **Example**:
```tsx
<Form {...form}><FormField name="email" render={({field}) => (<FormItem><FormLabel>Email</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>)} /></Form>
```

### Composition

#### Logo
- **File**: `components/ui/logo.tsx`
- **Exports**: `Logo`, `logoVariants`
- **Description**: Greyhaven logo SVG. Size: sm/md/lg/xl. Variant: color (orange icon + foreground text) or monochrome (all foreground).
- **Props**: `size?: "sm" | "md" | "lg" | "xl"; variant?: "color" | "monochrome"`
- **Example**:
```tsx
<Logo size="md" variant="color" />
```

#### Hero
- **File**: `components/ui/hero.tsx`
- **Exports**: `Hero`, `heroVariants`
- **Description**: Full-width hero section. Variants: centered, left-aligned, split (text + media). Heading in Source Serif, subheading in sans.
- **Props**: `variant?: "centered" | "left-aligned" | "split"; background?: "default" | "muted" | "accent" | "dark"; heading: ReactNode; subheading?: ReactNode; actions?: ReactNode; media?: ReactNode`
- **Example**:
```tsx
<Hero variant="centered" heading="Build something great" subheading="With the Greyhaven Design System" actions={<Button>Get Started</Button>} />
```

#### CTASection
- **File**: `components/ui/cta-section.tsx`
- **Exports**: `CTASection`, `ctaSectionVariants`
- **Description**: Call-to-action section block. Centered or left-aligned, with heading, description, and action buttons.
- **Props**: `variant?: "centered" | "left-aligned"; background?: "default" | "muted" | "accent" | "subtle"; heading: ReactNode; description?: ReactNode; actions?: ReactNode`
- **Example**:
```tsx
<CTASection heading="Ready to start?" description="Join thousands of developers" actions={<Button>Sign up free</Button>} />
```

#### Section
- **File**: `components/ui/section.tsx`
- **Exports**: `Section`, `sectionVariants`
- **Description**: Titled content section with spacing. py-10 internal padding. Colored variants (highlighted, accent) get my-8 vertical margin so they visually detach from adjacent sections; default has no margin so same-bg siblings flow seamlessly.
- **Props**: `variant?: "default" | "highlighted" | "accent"; width?: "narrow" | "default" | "wide" | "full"; title?: string; description?: string`
- **Example**:
```tsx
<Section title="Features" description="What we offer" width="wide">Content</Section>
```

#### Footer
- **File**: `components/ui/footer.tsx`
- **Exports**: `Footer`, `footerVariants`
- **Description**: Page footer. Minimal (single row) or full (multi-column with link groups).
- **Props**: `variant?: "minimal" | "full"; logo?: ReactNode; copyright?: ReactNode; linkGroups?: FooterLinkGroup[]; actions?: ReactNode`
- **Example**:
```tsx
<Footer variant="minimal" copyright="&copy; 2024 Greyhaven" />
```

#### PageLayout
- **File**: `components/ui/page-layout.tsx`
- **Exports**: `PageLayout`
- **Description**: Full page shell composing Navbar + main content + optional sidebar + Footer. Auto-offsets for fixed navbar.
- **Props**: `navbar?: ReactNode; sidebar?: ReactNode; footer?: ReactNode`
- **Example**:
```tsx
<PageLayout navbar={<Navbar />} footer={<Footer />}>Main content</PageLayout>
```

---

## Composition Rules

- **Never override component sizing via `className`**: Each component exposes `size` / `variant` props for a reason. Reach for those first. Overriding font-size, padding, or height with arbitrary Tailwind classes (`text-sm`, `px-3`, `py-1`, etc.) fragments the design system. If no variant fits, add a new `size`/`variant` to the component — don't one-off patch it at the call site.
- **Minimum font size is `text-xs` (12px)**: Anything smaller fails accessibility/readability minimums. If you genuinely need smaller text for a specific reason (e.g., a data-dense legend), add an explicit `// justification: ...` comment at the call site. Default answer is: use `text-xs`.
- **Card spacing**: `gap-6` between cards, `p-6` internal padding
- **Section rhythm**: `py-10` internal padding per section. Colored sections add `my-8` to detach from neighbors
- **Button placement**: Primary action right, secondary left
- **Form layout**: Vertical stack with `gap-4`, labels above inputs
- **Navbar**: Fixed top, `z-50`, `h-16`, logo left, nav center, actions right
- **Typography pairing**: Serif (`font-serif`) for content headings, sans (`font-sans`) for UI labels/buttons
- **Color restraint**: Trust the default component variants for orange accent -- they apply it at the right scale. Don't apply `bg-primary` to large surfaces, containers, or section backgrounds
- **Focus pattern**: `focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]`
- **Disabled pattern**: `disabled:pointer-events-none disabled:opacity-50`
- **Aria-invalid pattern**: `aria-invalid:ring-destructive/20 aria-invalid:border-destructive`
- **Slot naming**: All components use `data-slot="component-name"`
- **Icon sizing**: `[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0`

---

## HTMX / Server-Rendered Usage

For projects that cannot use React (HTMX, Django templates, Rails ERB, Go `html/template`, Astro SSR, etc.), the design system ships a framework-agnostic CSS layer: `dist/greyhaven.htmx.css`.

### What it is

An auto-generated stylesheet derived from `components/ui/*.tsx`. Every `data-slot` attribute gets a `@layer components` rule. `cva` variants become attribute selectors (`[data-variant=...]`, `[data-size=...]`). Default variants apply via `:not([data-variant])` so consumers can omit the attribute.

### Usage

1. Install: `./skill/install.sh /path/to/project --htmx-css`
2. Import in your Tailwind v4 input CSS: `@import "./greyhaven.htmx.css";`
3. Emit HTML with `data-slot` / `data-variant` / `data-size` attributes:

```html
<div data-slot="card">
  <div data-slot="card-header">
    <div data-slot="card-title">Title</div>
    <div data-slot="card-description">Description</div>
  </div>
  <div data-slot="card-content">Body</div>
</div>

<button data-slot="button" data-variant="default">Save</button>
<button data-slot="button" data-variant="outline" data-size="sm">Cancel</button>

<span data-slot="badge" data-variant="success">Active</span>
```

### Scope

- **Fully static** (pure CSS, no JS): Card, Button, Badge, Input, Label, Textarea, Table, Separator, Code, Kbd, Progress, Avatar, Skeleton, Alert, Pagination, Breadcrumb, Navbar (solid variant), Spinner, AspectRatio, Empty, Hero, Section, Footer, CtaSection, ButtonGroup, InputGroup, Toast.
- **Visual-only** (CSS is correct but needs your own state JS): Dialog, Dropdown, Popover, Select, Combobox, Accordion, Tabs, Tooltip, Drawer, Sheet, Sidebar, Collapsible, NavigationMenu, Menubar, ContextMenu, HoverCard, Command, AlertDialog, InputOtp, Carousel. Pair with Alpine.js (`x-data`, `x-show`, `@click`) or native HTML primitives (`<dialog>`, `<details>`).

### Regenerate

```bash
pnpm htmx-css:build
```


---

## Extension Protocol

When adding new components to the system:

1. **Use CVA** for variants (`class-variance-authority`)
2. **Accept HTML element props** via spread: `React.ComponentProps<'div'>`
3. **Use `data-slot`** attribute: `data-slot="component-name"`
4. **Use `cn()`** from `@/lib/utils` for class merging
5. **Follow focus/disabled/aria patterns** from existing components
6. **Use semantic tokens only** -- never raw hex colors
7. **Support `asChild`** via `@radix-ui/react-slot` for polymorphism where appropriate
8. **Add to Storybook** with `tags: ['autodocs']` and all variant stories
9. **Add to `lib/catalog.ts`** so MCP server and SKILL.md pick it up automatically
10. **Run `pnpm skill:build`** to regenerate this file

### Template

```tsx
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const myComponentVariants = cva('base-classes', {
  variants: {
    variant: { default: 'default-classes' },
    size: { default: 'size-classes' },
  },
  defaultVariants: { variant: 'default', size: 'default' },
})

function MyComponent({
  className, variant, size, ...props
}: React.ComponentProps<'div'> & VariantProps<typeof myComponentVariants>) {
  return (
    <div
      data-slot="my-component"
      className={cn(myComponentVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { MyComponent, myComponentVariants }
```
