Files
2026-04-16 11:43:32 -05:00

34 KiB

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:

/* 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.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:
<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:
<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:
<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>

Code

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

  • 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>
  • 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="&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:
<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

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

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 }