# Greyhaven Design System -- Claude Skill > **Auto-generated** by `scripts/generate-skill.ts` -- DO NOT EDIT by hand. > Re-generate: `pnpm skill:build` > > **Components**: 37 | **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.7}` | Dark muted | | `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 (37 components) All components live in `components/ui/`. Import with `@/components/ui/`. ### 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 ``` #### Badge - **File**: `components/ui/badge.tsx` - **Exports**: `Badge`, `badgeVariants` - **Description**: Status indicator / tag. Variants include default, secondary, outline, destructive, success, warning, info, plus channel pills (WhatsApp, Email, Telegram, Zulip). - **Props**: `variant?: "default" | "secondary" | "destructive" | "outline" | "success" | "warning" | "info" | ...; asChild?: boolean` - **Example**: ```tsx Active ``` #### 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 ``` #### Textarea - **File**: `components/ui/textarea.tsx` - **Exports**: `Textarea` - **Description**: Multi-line text input. - **Props**: `All standard HTML textarea props` - **Example**: ```tsx