31 KiB
31 KiB
Greyhaven Design System
Auto-generated by
scripts/generate-skill.ts-- DO NOT EDIT by hand. Re-generate:pnpm skill:buildin the design system repo.This file provides AI coding assistants (Cursor, GitHub Copilot, Windsurf, Codeium, etc.) with full context about the Greyhaven Design System.
How to Use This
When building UI in this project, follow the Greyhaven Design System:
- Import components from
components/ui/(or@/components/ui/with alias) - Use semantic Tailwind classes (
bg-primary,text-foreground,border-border) -- never raw hex colors - Use
font-sans(Aspekta) for UI elements,font-serif(Source Serif) for content - Orange (
#D95E2A/bg-primary) is the ONLY accent color -- use sparingly - All components are framework-agnostic React (no Next.js imports)
Design Philosophy
- 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.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.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 |
4rem |
Vertical padding between major sections (py-16) |
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-16. Variants: solid, transparent, minimal. Logo left, nav center, actions right. Mobile hamburger.
- Props:
variant?: "solid" | "transparent" | "minimal"; logo?: ReactNode; actions?: ReactNode - Example:
<Navbar variant="solid" logo={<Logo size="sm" />}><NavbarLink href="/">Home</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-16 between sections.
- 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-16between major page sections - 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: Orange ONLY for primary actions and key emphasis -- never decorative
- 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 }