34 KiB
Greyhaven Design System -- Claude Skill
Auto-generated by
scripts/generate-skill.ts-- DO NOT EDIT by hand. Re-generate:pnpm skill:buildComponents: 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
.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.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
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
- 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:
- Use CVA for variants (
class-variance-authority)
- Accept HTML element props via spread:
React.ComponentProps<'div'>
- Use
data-slot attribute: data-slot="component-name"
- Use
cn() from @/lib/utils for class merging
- Follow focus/disabled/aria patterns from existing components
- Use semantic tokens only -- never raw hex colors
- Support
asChild via @radix-ui/react-slot for polymorphism where appropriate
- Add to Storybook with
tags: ['autodocs'] and all variant stories
- Add to
lib/catalog.ts so MCP server and SKILL.md pick it up automatically
- 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 }