/** * Greyhaven Design System — Shared Component Catalog & Token Utilities * * Single source of truth consumed by: * - MCP server (mcp/server.ts) * - SKILL.md generator (scripts/generate-skill.ts) */ import * as fs from 'fs' import * as path from 'path' // --------------------------------------------------------------------------- // Token utilities // --------------------------------------------------------------------------- export const TOKEN_CATEGORIES = ['color', 'typography', 'spacing', 'radii', 'shadows', 'motion'] as const export type TokenCategory = (typeof TOKEN_CATEGORIES)[number] export interface FlatToken { path: string value: unknown type?: string description?: string } export function loadTokenFile(root: string, name: string): Record { const filePath = path.join(root, 'tokens', `${name}.json`) return JSON.parse(fs.readFileSync(filePath, 'utf-8')) } export function flattenTokens( obj: Record, prefix = '', ): FlatToken[] { const results: FlatToken[] = [] for (const [key, val] of Object.entries(obj)) { if (key.startsWith('$')) continue const currentPath = prefix ? `${prefix}.${key}` : key const node = val as Record if (node && typeof node === 'object' && '$value' in node) { results.push({ path: currentPath, value: node.$value, type: node.$type as string | undefined, description: node.$description as string | undefined, }) } else if (node && typeof node === 'object') { results.push(...flattenTokens(node, currentPath)) } } return results } export function getTokens(root: string, category?: string): FlatToken[] { if (category && TOKEN_CATEGORIES.includes(category as TokenCategory)) { const data = loadTokenFile(root, category) return flattenTokens(data) } const all: FlatToken[] = [] for (const cat of TOKEN_CATEGORIES) { try { const data = loadTokenFile(root, cat) all.push(...flattenTokens(data)) } catch { // skip missing files } } return all } // --------------------------------------------------------------------------- // Component catalog // --------------------------------------------------------------------------- export interface ComponentSpec { name: string file: string category: string exports: string[] description: string props: string example: string } export const COMPONENT_CATALOG: ComponentSpec[] = [ // ── Primitives ────────────────────────────────────────────────────────── { name: 'Button', file: 'components/ui/button.tsx', category: 'primitives', 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: '', }, { name: 'Badge', file: 'components/ui/badge.tsx', category: 'primitives', 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: 'Active', }, { name: 'Input', file: 'components/ui/input.tsx', category: 'primitives', exports: ['Input'], description: 'Text input field with focus ring, disabled, and aria-invalid states.', props: 'All standard HTML input props', example: '', }, { name: 'Textarea', file: 'components/ui/textarea.tsx', category: 'primitives', exports: ['Textarea'], description: 'Multi-line text input.', props: 'All standard HTML textarea props', example: '