32 KiB
Greyhaven Design System -- Claude Skill
Auto-generated by
scripts/generate-skill.ts-- DO NOT EDIT by hand. Re-generate:pnpm skill:buildComponents: 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
.darkclass. - 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:
/* 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
--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/<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:
<Button variant="default" size="default">Click me</Button>
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:
<Badge variant="success">Active</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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<Toggle aria-label="Bold"><BoldIcon /></Toggle>
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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<Footer variant="minimal" copyright="© 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:
<PageLayout navbar={<Navbar />} footer={<Footer />}>Main content</PageLayout>
Composition Rules
- Card spacing:
gap-6between cards,p-6internal padding - Section rhythm:
py-10internal padding per section. Colored sections addmy-8to 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-primaryto 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
Extension Protocol
When adding new components to the system:
- Use CVA for variants (
class-variance-authority) - Accept HTML element props via spread:
React.ComponentProps<'div'> - Use
data-slotattribute:data-slot="component-name" - Use
cn()from@/lib/utilsfor class merging - Follow focus/disabled/aria patterns from existing components
- Use semantic tokens only -- never raw hex colors
- Support
asChildvia@radix-ui/react-slotfor polymorphism where appropriate - Add to Storybook with
tags: ['autodocs']and all variant stories - Add to
lib/catalog.tsso MCP server and SKILL.md pick it up automatically - Run
pnpm skill:buildto regenerate this file
Template
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 }