diff --git a/README.md b/README.md index 73ce57f..2a63c0b 100644 --- a/README.md +++ b/README.md @@ -55,20 +55,34 @@ greyhaven-design-system/ ## Using the Design System with AI -The design system provides two complementary ways for AI agents to consume it: +The design system provides three complementary ways for AI agents to consume it: -| | Claude Skill (SKILL.md) | MCP Server | -|---|---|---| -| **What it is** | A single markdown file with all tokens, components, and rules | A running process that reads source files in real-time | -| **Stays in sync** | Yes -- auto-generated from the same token files and component catalog | Yes -- reads source at runtime | -| **Setup** | Copy/symlink one file | Start a server process | -| **Best for** | Claude Code sessions, quick context | Programmatic access, validation, any MCP-compatible agent | +| | Claude Skill (SKILL.md) | AGENT.md | MCP Server | +|---|---|---|---| +| **What it is** | Claude Code skill file | Generic AI agent instructions | Running process with tools | +| **Stays in sync** | Yes -- auto-generated | Yes -- auto-generated | Yes -- reads source at runtime | +| **Setup** | Symlink to `.claude/skills/` | Symlink to project root | Configure in settings | +| **Best for** | Claude Code | Cursor, Copilot, Windsurf, Codeium | Programmatic access, validation | -Both read from the same sources (`tokens/*.json` and `lib/catalog.ts`), so they always agree. +All three read from the same sources (`tokens/*.json` and `lib/catalog.ts`), so they always agree. + +### Quick Install (all at once) + +The install script sets up everything -- Claude Skill, AGENT.md, and fonts: + +```bash +./skill/install.sh /path/to/your/project +``` + +This will: +1. Symlink `SKILL.md` into `.claude/skills/` (for Claude Code) +2. Symlink `AGENT.md` into the project root (for Cursor, Copilot, etc.) +3. Copy Aspekta font files into `public/fonts/` +4. Print CSS import instructions ### Option A: Claude Skill (SKILL.md) -The skill file gives any Claude Code session full design system context -- tokens, all components with props/examples, composition rules, and the extension protocol. +The skill file gives any Claude Code session full design system context -- tokens, all components with props/examples, composition rules, font setup, and the extension protocol. **Install into a consuming project:** @@ -90,7 +104,24 @@ pnpm skill:build This is run automatically as part of `pnpm build`. If you add a component, add it to `lib/catalog.ts` and regenerate. -### Option B: MCP Server +### Option B: AGENT.md (non-Claude AI agents) + +For Cursor, GitHub Copilot, Windsurf, Codeium, and other AI coding assistants that read project-root markdown files: + +```bash +# Via install script (also sets up fonts + Claude Skill) +./skill/install.sh /path/to/your/project + +# Or manually +ln -sf /absolute/path/to/greyhaven-design-system/skill/AGENT.md \ + /path/to/your/project/AGENT.md +``` + +Some tools use different file names. You can also symlink as: +- `.cursorrules` (Cursor) +- `.github/copilot-instructions.md` (GitHub Copilot) + +### Option C: MCP Server The MCP server provides 5 tools for programmatic access: diff --git a/app/globals.css b/app/globals.css index 49be907..57d21c2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,6 +3,17 @@ @import './tokens/tokens-light.css'; @import './tokens/tokens-dark.css'; +/* Aspekta — self-hosted sans font (canonical UI typeface) */ +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 100; font-display: swap; src: url('/fonts/Aspekta-100.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 200; font-display: swap; src: url('/fonts/Aspekta-200.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 300; font-display: swap; src: url('/fonts/Aspekta-300.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/Aspekta-400.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/Aspekta-500.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/Aspekta-600.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/Aspekta-700.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/Aspekta-800.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-style: normal; font-weight: 900; font-display: swap; src: url('/fonts/Aspekta-900.woff2') format('woff2'); } + @custom-variant dark (&:is(.dark *)); /* ============================================================================= @@ -30,8 +41,8 @@ ============================================================================= */ @theme inline { - /* Typography — Aspekta is the canonical sans font, Inter is fallback */ - --font-sans: 'Aspekta', var(--font-inter, 'Inter'), ui-sans-serif, system-ui, sans-serif; + /* Typography — Aspekta (self-hosted) is the canonical sans font */ + --font-sans: 'Aspekta', ui-sans-serif, system-ui, sans-serif; --font-serif: var(--font-source-serif, 'Source Serif 4'), 'Source Serif Pro', Georgia, serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; diff --git a/app/layout.tsx b/app/layout.tsx index b9a7b25..2aa863b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,23 +1,18 @@ import type { Metadata } from 'next' -import { Source_Serif_4, Inter } from 'next/font/google' +import { Source_Serif_4 } from 'next/font/google' import './globals.css' // Primary typeface: Source Serif Pro (using Source Serif 4 which is the updated version) // Used for headings, body text, and reading content -const sourceSerif = Source_Serif_4({ +const sourceSerif = Source_Serif_4({ subsets: ["latin"], variable: '--font-source-serif', display: 'swap', }) -// Secondary typeface: Inter (Aspekta alternative from Google Fonts) -// Aspekta is the brand typeface, Inter is a suitable system alternative +// Secondary typeface: Aspekta (self-hosted in public/fonts/) +// Loaded via @font-face in globals.css — no Next.js font loader needed // Used for UI labels, nav, buttons, small utility text -const inter = Inter({ - subsets: ["latin"], - variable: '--font-inter', - display: 'swap', -}) export const metadata: Metadata = { title: 'Greyhaven Design System', @@ -48,7 +43,7 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - + {children} diff --git a/app/tokens/TOKENS.md b/app/tokens/TOKENS.md index de5168b..76956f3 100644 --- a/app/tokens/TOKENS.md +++ b/app/tokens/TOKENS.md @@ -144,7 +144,7 @@ | Token | Value | Description | |-------|-------|-------------| -| `typography.fontFamily.sans` | `Aspekta, Inter, ui-sans-serif, system-ui, sans-serif` | UI labels, buttons, nav, forms — Aspekta primary, Inter fallback | +| `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 | diff --git a/public/fonts/Aspekta-100.woff2 b/public/fonts/Aspekta-100.woff2 new file mode 100644 index 0000000..1a64458 Binary files /dev/null and b/public/fonts/Aspekta-100.woff2 differ diff --git a/public/fonts/Aspekta-1000.woff2 b/public/fonts/Aspekta-1000.woff2 new file mode 100644 index 0000000..56a8de5 Binary files /dev/null and b/public/fonts/Aspekta-1000.woff2 differ diff --git a/public/fonts/Aspekta-150.woff2 b/public/fonts/Aspekta-150.woff2 new file mode 100644 index 0000000..9dbf773 Binary files /dev/null and b/public/fonts/Aspekta-150.woff2 differ diff --git a/public/fonts/Aspekta-200.woff2 b/public/fonts/Aspekta-200.woff2 new file mode 100644 index 0000000..47fbe73 Binary files /dev/null and b/public/fonts/Aspekta-200.woff2 differ diff --git a/public/fonts/Aspekta-250.woff2 b/public/fonts/Aspekta-250.woff2 new file mode 100644 index 0000000..aa4b382 Binary files /dev/null and b/public/fonts/Aspekta-250.woff2 differ diff --git a/public/fonts/Aspekta-300.woff2 b/public/fonts/Aspekta-300.woff2 new file mode 100644 index 0000000..455e882 Binary files /dev/null and b/public/fonts/Aspekta-300.woff2 differ diff --git a/public/fonts/Aspekta-350.woff2 b/public/fonts/Aspekta-350.woff2 new file mode 100644 index 0000000..f2e25ea Binary files /dev/null and b/public/fonts/Aspekta-350.woff2 differ diff --git a/public/fonts/Aspekta-400.woff2 b/public/fonts/Aspekta-400.woff2 new file mode 100644 index 0000000..764b50c Binary files /dev/null and b/public/fonts/Aspekta-400.woff2 differ diff --git a/public/fonts/Aspekta-450.woff2 b/public/fonts/Aspekta-450.woff2 new file mode 100644 index 0000000..ca48199 Binary files /dev/null and b/public/fonts/Aspekta-450.woff2 differ diff --git a/public/fonts/Aspekta-50.woff2 b/public/fonts/Aspekta-50.woff2 new file mode 100644 index 0000000..fb1e140 Binary files /dev/null and b/public/fonts/Aspekta-50.woff2 differ diff --git a/public/fonts/Aspekta-500.woff2 b/public/fonts/Aspekta-500.woff2 new file mode 100644 index 0000000..ba95918 Binary files /dev/null and b/public/fonts/Aspekta-500.woff2 differ diff --git a/public/fonts/Aspekta-550.woff2 b/public/fonts/Aspekta-550.woff2 new file mode 100644 index 0000000..5d84c2c Binary files /dev/null and b/public/fonts/Aspekta-550.woff2 differ diff --git a/public/fonts/Aspekta-600.woff2 b/public/fonts/Aspekta-600.woff2 new file mode 100644 index 0000000..ea5728e Binary files /dev/null and b/public/fonts/Aspekta-600.woff2 differ diff --git a/public/fonts/Aspekta-650.woff2 b/public/fonts/Aspekta-650.woff2 new file mode 100644 index 0000000..8f72f51 Binary files /dev/null and b/public/fonts/Aspekta-650.woff2 differ diff --git a/public/fonts/Aspekta-700.woff2 b/public/fonts/Aspekta-700.woff2 new file mode 100644 index 0000000..b9342d8 Binary files /dev/null and b/public/fonts/Aspekta-700.woff2 differ diff --git a/public/fonts/Aspekta-750.woff2 b/public/fonts/Aspekta-750.woff2 new file mode 100644 index 0000000..57e36c9 Binary files /dev/null and b/public/fonts/Aspekta-750.woff2 differ diff --git a/public/fonts/Aspekta-800.woff2 b/public/fonts/Aspekta-800.woff2 new file mode 100644 index 0000000..72fea9f Binary files /dev/null and b/public/fonts/Aspekta-800.woff2 differ diff --git a/public/fonts/Aspekta-850.woff2 b/public/fonts/Aspekta-850.woff2 new file mode 100644 index 0000000..df40fbf Binary files /dev/null and b/public/fonts/Aspekta-850.woff2 differ diff --git a/public/fonts/Aspekta-900.woff2 b/public/fonts/Aspekta-900.woff2 new file mode 100644 index 0000000..6e97574 Binary files /dev/null and b/public/fonts/Aspekta-900.woff2 differ diff --git a/public/fonts/Aspekta-950.woff2 b/public/fonts/Aspekta-950.woff2 new file mode 100644 index 0000000..a463756 Binary files /dev/null and b/public/fonts/Aspekta-950.woff2 differ diff --git a/public/fonts/font-face.css b/public/fonts/font-face.css new file mode 100644 index 0000000..b116bb2 --- /dev/null +++ b/public/fonts/font-face.css @@ -0,0 +1,161 @@ +/*! Aspekta | OFL v1.1 License | Ivo Dolenc (c) 2025 | https://github.com/ivodolenc/aspekta */ + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 50; + font-display: swap; + src: url('Aspekta-50.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url('Aspekta-100.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 150; + font-display: swap; + src: url('Aspekta-150.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url('Aspekta-200.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 250; + font-display: swap; + src: url('Aspekta-250.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('Aspekta-300.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 350; + font-display: swap; + src: url('Aspekta-350.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('Aspekta-400.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 450; + font-display: swap; + src: url('Aspekta-450.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('Aspekta-500.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 550; + font-display: swap; + src: url('Aspekta-550.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('Aspekta-600.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 650; + font-display: swap; + src: url('Aspekta-650.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('Aspekta-700.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 750; + font-display: swap; + src: url('Aspekta-750.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url('Aspekta-800.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 850; + font-display: swap; + src: url('Aspekta-850.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url('Aspekta-900.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 950; + font-display: swap; + src: url('Aspekta-950.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 1000; + font-display: swap; + src: url('Aspekta-1000.woff2') format('woff2'); +} diff --git a/scripts/generate-skill.ts b/scripts/generate-skill.ts index 6df10cc..511faa1 100644 --- a/scripts/generate-skill.ts +++ b/scripts/generate-skill.ts @@ -1,11 +1,15 @@ #!/usr/bin/env npx tsx /** - * Generates skill/SKILL.md from the shared component catalog and - * W3C DTCG token files. Run via `pnpm skill:build`. + * Generates skill/SKILL.md and skill/AGENT.md from the shared component + * catalog and W3C DTCG token files. Run via `pnpm skill:build`. * * Both the MCP server and this script read from lib/catalog.ts and - * tokens/*.json, so SKILL.md always stays in sync. + * tokens/*.json, so all outputs stay in sync. + * + * Outputs: + * skill/SKILL.md — Claude Code skill (loaded via .claude/skills/) + * skill/AGENT.md — Generic AI agent instructions (Cursor, Copilot, etc.) */ import * as fs from 'fs' @@ -13,7 +17,6 @@ import * as path from 'path' import { fileURLToPath } from 'url' import { COMPONENT_CATALOG, - getTokens, loadTokenFile, flattenTokens, TOKEN_CATEGORIES, @@ -28,11 +31,6 @@ const ROOT = path.resolve(__dirname, '..') // Helpers // --------------------------------------------------------------------------- -function hex(token: FlatToken): string { - const v = token.value - return typeof v === 'string' ? v : JSON.stringify(v) -} - function tokenTable(tokens: FlatToken[]): string { const lines = ['| Token | Value | Description |', '|-------|-------|-------------|'] for (const t of tokens) { @@ -47,31 +45,14 @@ function componentCount(): number { } // --------------------------------------------------------------------------- -// Build sections +// Shared content blocks (used by both SKILL.md and AGENT.md) // --------------------------------------------------------------------------- -function buildHeader(): string { - return `# Greyhaven Design System -- Claude Skill - -> **Auto-generated** by \`scripts/generate-skill.ts\` -- DO NOT EDIT by hand. -> Re-generate: \`pnpm skill:build\` -> -> **Components**: ${componentCount()} | **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. -` -} - function buildDesignPhilosophy(): string { - return ` ---- - -## 1. Design Philosophy + return `## 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/Inter (sans) for UI labels, buttons, navigation, and form elements. +- **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. @@ -79,10 +60,50 @@ function buildDesignPhilosophy(): string { ` } +function buildFontSetup(): string { + return `## 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: + +\`\`\`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 + +\`\`\`css +--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\` +` +} + function buildTokenReference(): string { const lines: string[] = [] - lines.push('\n---\n') - lines.push('## 2. Token Quick Reference\n') + lines.push('## Token Quick Reference\n') lines.push('Source of truth: `tokens/*.json` (W3C DTCG format).\n') for (const cat of TOKEN_CATEGORIES) { @@ -105,16 +126,13 @@ function buildTokenReference(): string { function buildComponentCatalog(): string { const lines: string[] = [] - lines.push('\n---\n') - lines.push(`## 3. Component Catalog (${componentCount()} components)\n`) + lines.push(`## Component Catalog (${componentCount()} components)\n`) lines.push('All components live in `components/ui/`. Import with `@/components/ui/`.\n') - // Group by category const categories = new Map() for (const c of COMPONENT_CATALOG) { - const cat = c.category - if (!categories.has(cat)) categories.set(cat, []) - categories.get(cat)!.push(c) + if (!categories.has(c.category)) categories.set(c.category, []) + categories.get(c.category)!.push(c) } const categoryOrder = ['primitives', 'layout', 'overlay', 'navigation', 'data', 'feedback', 'form', 'composition'] @@ -144,10 +162,7 @@ function buildComponentCatalog(): string { } function buildCompositionRules(): string { - return ` ---- - -## 4. Composition Rules + return `## Composition Rules - **Card spacing**: \`gap-6\` between cards, \`p-6\` internal padding - **Section rhythm**: \`py-16\` between major page sections @@ -165,10 +180,7 @@ function buildCompositionRules(): string { } function buildExtensionProtocol(): string { - return ` ---- - -## 5. Extension Protocol + return `## Extension Protocol When adding new components to the system: @@ -192,24 +204,14 @@ import { cn } from '@/lib/utils' const myComponentVariants = cva('base-classes', { variants: { - variant: { - default: 'default-classes', - }, - size: { - default: 'size-classes', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', + variant: { default: 'default-classes' }, + size: { default: 'size-classes' }, }, + defaultVariants: { variant: 'default', size: 'default' }, }) function MyComponent({ - className, - variant, - size, - ...props + className, variant, size, ...props }: React.ComponentProps<'div'> & VariantProps) { return (
**Auto-generated** by \`scripts/generate-skill.ts\` -- DO NOT EDIT by hand. +> Re-generate: \`pnpm skill:build\` +> +> **Components**: ${componentCount()} | **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. +`, + '---\n', + buildDesignPhilosophy(), + '---\n', + buildFontSetup(), + '---\n', + buildTokenReference(), + '---\n', + buildComponentCatalog(), + '---\n', + buildCompositionRules(), + '---\n', + buildExtensionProtocol(), + ].join('\n') +} + +// --------------------------------------------------------------------------- +// AGENT.md (non-Claude AI agents: Cursor, Copilot, Windsurf, etc.) +// --------------------------------------------------------------------------- + +function generateAgent(): string { + return [ + `# Greyhaven Design System + +> **Auto-generated** by \`scripts/generate-skill.ts\` -- DO NOT EDIT by hand. +> Re-generate: \`pnpm skill:build\` in 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) +`, + '---\n', + buildDesignPhilosophy(), + '---\n', + buildFontSetup(), + '---\n', + buildTokenReference(), + '---\n', + buildComponentCatalog(), + '---\n', + buildCompositionRules(), + '---\n', + buildExtensionProtocol(), + ].join('\n') +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- function main() { - const skill = [ - buildHeader(), - buildDesignPhilosophy(), - buildTokenReference(), - buildComponentCatalog(), - buildCompositionRules(), - buildExtensionProtocol(), - ].join('\n') + const outDir = path.join(ROOT, 'skill') + fs.mkdirSync(outDir, { recursive: true }) - const outPath = path.join(ROOT, 'skill', 'SKILL.md') - fs.mkdirSync(path.dirname(outPath), { recursive: true }) - fs.writeFileSync(outPath, skill, 'utf-8') + // SKILL.md + const skill = generateSkill() + const skillPath = path.join(outDir, 'SKILL.md') + fs.writeFileSync(skillPath, skill, 'utf-8') + const skillLines = skill.split('\n').length + console.log(`skill/SKILL.md generated (${skillLines} lines, ${componentCount()} components)`) - const lineCount = skill.split('\n').length - console.log(`skill/SKILL.md generated (${lineCount} lines, ${componentCount()} components)`) + // AGENT.md + const agent = generateAgent() + const agentPath = path.join(outDir, 'AGENT.md') + fs.writeFileSync(agentPath, agent, 'utf-8') + const agentLines = agent.split('\n').length + console.log(`skill/AGENT.md generated (${agentLines} lines, ${componentCount()} components)`) } main() diff --git a/skill/AGENT.md b/skill/AGENT.md new file mode 100644 index 0000000..44b90bb --- /dev/null +++ b/skill/AGENT.md @@ -0,0 +1,693 @@ +# Greyhaven Design System + +> **Auto-generated** by `scripts/generate-skill.ts` -- DO NOT EDIT by hand. +> Re-generate: `pnpm skill:build` in 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 `.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: + +```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 + +```css +--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/`. + +### 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**: +```tsx + +``` + +#### 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**: +```tsx +Active +``` + +#### 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**: +```tsx + +``` + +#### Textarea +- **File**: `components/ui/textarea.tsx` +- **Exports**: `Textarea` +- **Description**: Multi-line text input. +- **Props**: `All standard HTML textarea props` +- **Example**: +```tsx +