design system token v0.2
This commit is contained in:
@@ -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/<name>`.\n')
|
||||
|
||||
// Group by category
|
||||
const categories = new Map<string, typeof COMPONENT_CATALOG>()
|
||||
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<typeof myComponentVariants>) {
|
||||
return (
|
||||
<div
|
||||
@@ -225,26 +227,97 @@ export { MyComponent, myComponentVariants }
|
||||
`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SKILL.md (Claude Code)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateSkill(): 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.
|
||||
`,
|
||||
'---\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()
|
||||
|
||||
Reference in New Issue
Block a user