Merge pull request 'design system + brand skill' (#2) from juan/design into main
Reviewed-on: #2
7
.gitignore
vendored
@@ -24,4 +24,9 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
next-env.d.ts
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
# llms
|
||||
vibedocs/*
|
||||
|
||||
17
.storybook/main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { StorybookConfig } from '@storybook/nextjs-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
'../stories/**/*.mdx',
|
||||
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-a11y',
|
||||
'@chromatic-com/storybook',
|
||||
],
|
||||
framework: '@storybook/nextjs-vite',
|
||||
staticDirs: ['../public'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
44
.storybook/preview.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Preview } from '@storybook/nextjs-vite'
|
||||
import '../app/globals.css'
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
a11y: {
|
||||
test: 'todo',
|
||||
},
|
||||
backgrounds: { disable: true },
|
||||
},
|
||||
globalTypes: {
|
||||
theme: {
|
||||
description: 'Theme',
|
||||
toolbar: {
|
||||
title: 'Theme',
|
||||
icon: 'paintbrush',
|
||||
items: [
|
||||
{ value: 'light', title: 'Light', icon: 'sun' },
|
||||
{ value: 'dark', title: 'Dark', icon: 'moon' },
|
||||
],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
initialGlobals: {
|
||||
theme: 'light',
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
const theme = context.globals.theme || 'light'
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(theme)
|
||||
return Story()
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default preview
|
||||
280
README.md
@@ -1,47 +1,269 @@
|
||||
# Greyhaven Design System
|
||||
|
||||
A modern design system built with Next.js, shadcn/ui, and Radix UI primitives.
|
||||
A framework-agnostic React component library built on Radix UI, Tailwind CSS v4, and shadcn/ui patterns. Designed for LLM consumption with a Claude Skill, MCP server, and Storybook documentation.
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start development server
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Start production server
|
||||
pnpm start
|
||||
pnpm install # Install dependencies
|
||||
pnpm dev # Start showcase dev server (Next.js)
|
||||
pnpm build # Tokens + SKILL.md + production build
|
||||
pnpm storybook # Component catalog on http://localhost:6006
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
greyhaven-design-system/
|
||||
├── app/ # Next.js app directory
|
||||
│ ├── layout.tsx # Root layout with fonts
|
||||
│ ├── page.tsx # Design system showcase
|
||||
│ └── globals.css # Global styles
|
||||
├── components/
|
||||
│ ├── ui/ # Reusable UI components (57 components)
|
||||
│ ├── design-system/ # Showcase components
|
||||
│ └── theme-provider.tsx # Theme context
|
||||
├── hooks/ # Custom React hooks
|
||||
├── components/ui/ # 37+ framework-agnostic React components
|
||||
├── tokens/ # W3C DTCG design tokens (source of truth)
|
||||
│ ├── color.json
|
||||
│ ├── typography.json
|
||||
│ ├── spacing.json
|
||||
│ ├── radii.json
|
||||
│ ├── shadows.json
|
||||
│ └── motion.json
|
||||
├── skill/ # AI skills
|
||||
│ ├── SKILL.md # Design system reference (auto-generated)
|
||||
│ ├── AGENTS.md # Project instructions (auto-generated)
|
||||
│ ├── BRAND.md # Voice/tone/messaging (hand-curated, opt-in)
|
||||
│ └── install.sh # Installer (supports --brand-skill flag)
|
||||
├── mcp/ # MCP server for AI agents
|
||||
│ └── server.ts
|
||||
├── stories/ # Storybook stories (23 files, 8 categories)
|
||||
├── lib/
|
||||
│ └── utils.ts # Utility functions
|
||||
├── styles/ # Additional styles
|
||||
└── public/ # Static assets
|
||||
│ ├── utils.ts # cn() utility
|
||||
│ └── catalog.ts # Shared component catalog (used by MCP + SKILL.md)
|
||||
├── scripts/
|
||||
│ └── generate-skill.ts # SKILL.md generator
|
||||
├── app/ # Next.js showcase app (demo only)
|
||||
└── style-dictionary.config.mjs
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** Next.js 16, React 19, TypeScript
|
||||
- **Styling:** Tailwind CSS 4, shadcn/ui, Radix UI
|
||||
- **Forms:** React Hook Form, Zod
|
||||
- **Theming:** next-themes (light/dark mode)
|
||||
- **Components**: React 19, Radix UI, Tailwind CSS 4, CVA, tailwind-merge, clsx
|
||||
- **Icons**: Lucide React
|
||||
- **Forms**: React Hook Form + Zod
|
||||
- **Tokens**: Style Dictionary v4 (W3C DTCG format)
|
||||
- **Docs**: Storybook 10, auto-generated SKILL.md
|
||||
- **AI Integration**: MCP server, Claude Skill
|
||||
|
||||
> **Framework-agnostic**: Components have zero Next.js imports. They work with Vite, Remix, Astro, CRA, or any React framework.
|
||||
|
||||
---
|
||||
|
||||
## Using the Design System with AI
|
||||
|
||||
The design system provides four things for AI agents:
|
||||
|
||||
| File | What it is | Where it goes |
|
||||
|------|-----------|---------------|
|
||||
| **SKILL.md** | Full design system reference (tokens, all components, composition rules, extension protocol) | `.claude/skills/` — works in Claude Code, Cursor, OpenCode, Anigravity, etc. |
|
||||
| **AGENTS.md** | Short project instructions telling the AI *how* to use the design system (follows the [agents.md](https://agents.md) convention) | Project root — copy as `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or `.github/copilot-instructions.md` |
|
||||
| **BRAND.md** *(opt-in)* | Voice/tone/messaging rules for generating marketing copy, CTAs, product explanations. Hand-curated from the brand guidelines. | `.claude/skills/greyhaven-brand.md` — opt in via `--brand-skill` flag |
|
||||
| **MCP Server** | Runtime tools for looking up components, validating colors, suggesting components, fetching brand rules, validating copy | Configured in `.mcp.json` |
|
||||
|
||||
SKILL.md and AGENTS.md are auto-generated from `tokens/*.json` and `lib/catalog.ts`. BRAND.md is hand-curated from `vibedocs/greyhaven-brand-system.md`.
|
||||
|
||||
### Quick Install (all at once)
|
||||
|
||||
```bash
|
||||
# Default: SKILL.md + AGENTS.md + fonts
|
||||
./skill/install.sh /path/to/your/project
|
||||
|
||||
# With brand skill: also install BRAND.md + logo SVGs
|
||||
./skill/install.sh /path/to/your/project --brand-skill
|
||||
```
|
||||
|
||||
This **copies** (not symlinks) the following into your project:
|
||||
1. `SKILL.md` → `.claude/skills/greyhaven-design-system.md` (full reference)
|
||||
2. `AGENTS.md` → project root (project-level instructions)
|
||||
3. Aspekta font files → `public/fonts/`
|
||||
|
||||
With `--brand-skill`, additionally:
|
||||
|
||||
4. `BRAND.md` → `.claude/skills/greyhaven-brand.md` (voice/tone/messaging)
|
||||
5. Logo SVGs → `public/logos/` (file names normalized: `gh-logo-positive-full-black.svg`, `gh-symbol-full-black.svg`, etc.)
|
||||
|
||||
The script also prints the CSS `@font-face` block and MCP server config to add next.
|
||||
|
||||
**Re-run the script after design system updates** to refresh your copies.
|
||||
|
||||
### SKILL.md (full reference)
|
||||
|
||||
The skill file gives any AI agent full design system context — every token, every component with props/variants/examples, composition rules, font setup, and the extension protocol.
|
||||
|
||||
**It's a global standard** — works with Claude Code, Cursor, OpenCode, Anigravity, and any tool that reads skill files.
|
||||
|
||||
```bash
|
||||
# Via install script (recommended — also handles fonts + AGENTS.md)
|
||||
./skill/install.sh /path/to/your/project
|
||||
|
||||
# Or copy manually
|
||||
mkdir -p /path/to/your/project/.claude/skills
|
||||
cp /path/to/greyhaven-design-system/skill/SKILL.md \
|
||||
/path/to/your/project/.claude/skills/greyhaven-design-system.md
|
||||
```
|
||||
|
||||
### AGENTS.md (project instructions)
|
||||
|
||||
Short, directive instructions that tell the AI agent *how* to work in the project — use TypeScript, use semantic tokens, reference the MCP tools, etc. Follows the [agents.md](https://agents.md) convention so it works with most AI coding tools out of the box.
|
||||
|
||||
**Copy it to your project root** under whichever name your tool reads:
|
||||
|
||||
```bash
|
||||
# Standard (agents.md convention — Cursor, OpenCode, Windsurf, Aider, etc.)
|
||||
cp /path/to/greyhaven-design-system/skill/AGENTS.md /path/to/your/project/AGENTS.md
|
||||
|
||||
# Claude Code
|
||||
cp /path/to/greyhaven-design-system/skill/AGENTS.md /path/to/your/project/CLAUDE.md
|
||||
|
||||
# Cursor (legacy)
|
||||
cp /path/to/greyhaven-design-system/skill/AGENTS.md /path/to/your/project/.cursorrules
|
||||
|
||||
# GitHub Copilot
|
||||
mkdir -p /path/to/your/project/.github
|
||||
cp /path/to/greyhaven-design-system/skill/AGENTS.md /path/to/your/project/.github/copilot-instructions.md
|
||||
```
|
||||
|
||||
Or use the install script, which copies `AGENTS.md` to the project root automatically.
|
||||
|
||||
### BRAND.md (voice, tone, messaging)
|
||||
|
||||
BRAND.md is an **opt-in** skill for projects that generate user-facing content — marketing copy, landing pages, CTAs, product explanations, emails. It codifies the Greyhaven brand voice: direct, plain-spoken, engineering-flavored, no hype, no sales language.
|
||||
|
||||
It's **not installed by default** because most projects only need the design system components, not brand voice rules.
|
||||
|
||||
**Install via the `--brand-skill` flag:**
|
||||
|
||||
```bash
|
||||
./skill/install.sh /path/to/your/project --brand-skill
|
||||
```
|
||||
|
||||
This adds:
|
||||
- `skill/BRAND.md` → `.claude/skills/greyhaven-brand.md` (brand skill)
|
||||
- Greyhaven logo SVGs → `public/logos/` (full logos + symbol-only + product lockups, in black and white variants, file names normalized)
|
||||
|
||||
Once installed, AI agents in your project can reference the brand skill when generating copy. The skill covers:
|
||||
- Core positioning and the three brand axes (containment, human-centered, engineered)
|
||||
- Tone of voice rules
|
||||
- Writing patterns (plain-language engineering, no hype)
|
||||
- Reasoning patterns (cause→effect, constraint→outcome, observation→explanation, finite scope→concrete result)
|
||||
- CTA guidance (good vs. bad patterns)
|
||||
- Logo usage rules
|
||||
- A self-check list to run before shipping any copy
|
||||
|
||||
### Option C: MCP Server
|
||||
|
||||
The MCP server provides 7 tools for programmatic access:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_tokens(category?)` | Returns token values (all, or filtered by: color, typography, spacing, radii, shadows, motion) |
|
||||
| `get_component(name)` | Returns full component spec + source code |
|
||||
| `list_components(category?)` | Lists components (all, or by: primitives, layout, overlay, navigation, data, feedback, form, composition) |
|
||||
| `validate_colors(code)` | Checks code for raw hex values that should use design tokens |
|
||||
| `suggest_component(description)` | Suggests components for a described UI need |
|
||||
| `get_brand_rules(section?)` | Returns brand voice/tone/messaging rules. Section can be: `positioning`, `axes`, `tone`, `writing-rules`, `reasoning-patterns`, `cta`, `logo`, `self-check`, or `all` |
|
||||
| `validate_copy(text)` | Lints marketing copy for hype words, sales language, vague superlatives, urgency framing, and exclamation marks |
|
||||
|
||||
Plus resources: `tokens://all`, `component://{name}` for each component, and `brand://guidelines` for the full brand skill.
|
||||
|
||||
**Run directly:**
|
||||
|
||||
```bash
|
||||
pnpm mcp:start
|
||||
```
|
||||
|
||||
**Install in Claude Code (`.mcp.json` in your project root):**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"greyhaven": {
|
||||
"command": "npx",
|
||||
"args": ["tsx", "/absolute/path/to/greyhaven-design-system/mcp/server.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Install in Claude Desktop (`claude_desktop_config.json`):**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"greyhaven": {
|
||||
"command": "npx",
|
||||
"args": ["tsx", "/absolute/path/to/greyhaven-design-system/mcp/server.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After adding, restart Claude Code / Claude Desktop. The tools will appear automatically.
|
||||
|
||||
**Test it works:**
|
||||
|
||||
```bash
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | pnpm mcp:start
|
||||
```
|
||||
|
||||
You should see a JSON response with `"serverInfo":{"name":"greyhaven-design-system"}`.
|
||||
|
||||
---
|
||||
|
||||
## Design Tokens
|
||||
|
||||
Tokens are defined in `tokens/*.json` using the [W3C Design Token Community Group](https://tr.designtokens.org/format/) format. Style Dictionary v4 generates:
|
||||
|
||||
| Output | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| CSS (light) | `app/tokens/tokens-light.css` | `:root` CSS custom properties |
|
||||
| CSS (dark) | `app/tokens/tokens-dark.css` | `.dark` CSS custom properties |
|
||||
| TypeScript | `app/tokens/tokens.ts` | Type-safe token constants |
|
||||
| Markdown | `app/tokens/TOKENS.md` | Reference doc |
|
||||
|
||||
```bash
|
||||
pnpm tokens:build # Regenerate all outputs from tokens/*.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Storybook
|
||||
|
||||
23 story files across 8 categories with autodocs, theme switching (light/dark via toolbar), and all component variants.
|
||||
|
||||
```bash
|
||||
pnpm storybook # Dev server on http://localhost:6006
|
||||
pnpm build-storybook # Static build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Component
|
||||
|
||||
1. Create `components/ui/my-component.tsx` following the CVA pattern (see `button.tsx`)
|
||||
2. Add it to the catalog in `lib/catalog.ts`
|
||||
3. Create a story in `stories/<Category>/MyComponent.stories.tsx`
|
||||
4. Run `pnpm skill:build` to regenerate SKILL.md (or just `pnpm build`)
|
||||
5. The MCP server picks it up automatically (reads `lib/catalog.ts` at runtime)
|
||||
|
||||
---
|
||||
|
||||
## Scripts Reference
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `pnpm dev` | Start Next.js showcase dev server |
|
||||
| `pnpm build` | Full build: tokens + SKILL.md + Next.js |
|
||||
| `pnpm storybook` | Storybook dev server on :6006 |
|
||||
| `pnpm build-storybook` | Static Storybook build |
|
||||
| `pnpm tokens:build` | Regenerate CSS/TS/MD from token JSON files |
|
||||
| `pnpm skill:build` | Regenerate skill/SKILL.md and skill/AGENTS.md from tokens + catalog |
|
||||
| `pnpm mcp:start` | Start the MCP server (stdio transport) |
|
||||
| `pnpm mcp:build` | Type-check MCP server |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
|
||||
156
app/globals.css
@@ -1,12 +1,28 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@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 *));
|
||||
|
||||
/* =============================================================================
|
||||
GREYHAVEN DESIGN TOKENS
|
||||
Based on Greyhaven Brand Guidelines v1.0
|
||||
|
||||
|
||||
Token values are auto-generated by Style Dictionary from tokens/*.json
|
||||
(W3C DTCG format). DO NOT hand-edit :root or .dark — edit the source
|
||||
token files and run `pnpm tokens:build` instead.
|
||||
|
||||
Color Philosophy:
|
||||
- Neutral and minimal scheme
|
||||
- Off-black, warm grey, and white form the base
|
||||
@@ -14,142 +30,23 @@
|
||||
- No gradients, no decorative color
|
||||
============================================================================= */
|
||||
|
||||
/* Radius — not color, kept inline */
|
||||
:root {
|
||||
/* Background & Foreground */
|
||||
--background: 240 240 236; /* Grey 1 #F0F0EC */
|
||||
--foreground: 22 22 20; /* Off-black #161614 */
|
||||
|
||||
/* Card */
|
||||
--card: 249 249 247; /* Off-white #F9F9F7 - elevated from background */
|
||||
--card-foreground: 22 22 20;
|
||||
|
||||
/* Popover */
|
||||
--popover: 249 249 247;
|
||||
--popover-foreground: 22 22 20;
|
||||
|
||||
/* Primary - Orange accent (used sparingly) */
|
||||
--primary: 217 94 42; /* Orange #D95E2A */
|
||||
--primary-foreground: 249 249 247; /* Off-white */
|
||||
|
||||
/* Secondary */
|
||||
--secondary: 240 240 236; /* Grey 1 #F0F0EC */
|
||||
--secondary-foreground: 47 47 44; /* Grey 8 #2F2F2C */
|
||||
|
||||
/* Muted */
|
||||
--muted: 240 240 236; /* Grey 1 #F0F0EC */
|
||||
--muted-foreground: 87 87 83; /* Grey 7 #575753 */
|
||||
|
||||
/* Accent - Subtle grey for hover states */
|
||||
--accent: 221 221 215; /* Grey 2 #DDDDD7 */
|
||||
--accent-foreground: 22 22 20; /* Off-black */
|
||||
|
||||
/* Destructive */
|
||||
--destructive: 180 50 50; /* Muted red for destructive actions */
|
||||
--destructive-foreground: 249 249 247;
|
||||
|
||||
/* Borders & Inputs */
|
||||
--border: 196 196 189; /* Grey 3 #C4C4BD */
|
||||
--input: 196 196 189; /* Grey 3 #C4C4BD */
|
||||
|
||||
/* Focus Ring */
|
||||
--ring: 217 94 42; /* Orange for focus states */
|
||||
|
||||
/* Charts - Warm neutral palette with orange accent */
|
||||
--chart-1: 217 94 42; /* Orange accent */
|
||||
--chart-2: 87 87 83; /* Grey 7 */
|
||||
--chart-3: 127 127 121; /* Grey 5 */
|
||||
--chart-4: 166 166 159; /* Grey 4 */
|
||||
--chart-5: 47 47 44; /* Grey 8 */
|
||||
|
||||
/* Radius - Tightened, no playful rounding */
|
||||
--radius: 0.375rem;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: 240 240 236;
|
||||
--sidebar-foreground: 22 22 20;
|
||||
--sidebar-primary: 217 94 42;
|
||||
--sidebar-primary-foreground: 249 249 247;
|
||||
--sidebar-accent: 196 196 189;
|
||||
--sidebar-accent-foreground: 22 22 20;
|
||||
--sidebar-border: 196 196 189;
|
||||
--sidebar-ring: 217 94 42;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
DARK THEME
|
||||
Guidelines: Negative/reverse usage via off-black and greys
|
||||
============================================================================= */
|
||||
|
||||
.dark {
|
||||
/* Background & Foreground */
|
||||
--background: 22 22 20; /* Off-black #161614 */
|
||||
--foreground: 249 249 247; /* Off-white #F9F9F7 */
|
||||
|
||||
/* Card */
|
||||
--card: 47 47 44; /* Grey 8 #2F2F2C */
|
||||
--card-foreground: 249 249 247;
|
||||
|
||||
/* Popover */
|
||||
--popover: 47 47 44;
|
||||
--popover-foreground: 249 249 247;
|
||||
|
||||
/* Primary - Orange accent (same in dark mode) */
|
||||
--primary: 217 94 42; /* Orange #D95E2A */
|
||||
--primary-foreground: 249 249 247;
|
||||
|
||||
/* Secondary */
|
||||
--secondary: 87 87 83; /* Grey 7 #575753 */
|
||||
--secondary-foreground: 249 249 247;
|
||||
|
||||
/* Muted */
|
||||
--muted: 87 87 83; /* Grey 7 #575753 */
|
||||
--muted-foreground: 196 196 189; /* Grey 3 #C4C4BD */
|
||||
|
||||
/* Accent - Subtle grey for hover states */
|
||||
--accent: 87 87 83; /* Grey 7 #575753 */
|
||||
--accent-foreground: 249 249 247;
|
||||
|
||||
/* Destructive */
|
||||
--destructive: 180 50 50;
|
||||
--destructive-foreground: 249 249 247;
|
||||
|
||||
/* Borders & Inputs */
|
||||
--border: 87 87 83; /* Grey 7 */
|
||||
--input: 87 87 83;
|
||||
|
||||
/* Focus Ring */
|
||||
--ring: 217 94 42;
|
||||
|
||||
/* Charts */
|
||||
--chart-1: 217 94 42;
|
||||
--chart-2: 196 196 189;
|
||||
--chart-3: 166 166 159;
|
||||
--chart-4: 127 127 121;
|
||||
--chart-5: 240 240 236;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: 47 47 44;
|
||||
--sidebar-foreground: 249 249 247;
|
||||
--sidebar-primary: 217 94 42;
|
||||
--sidebar-primary-foreground: 249 249 247;
|
||||
--sidebar-accent: 87 87 83;
|
||||
--sidebar-accent-foreground: 249 249 247;
|
||||
--sidebar-border: 87 87 83;
|
||||
--sidebar-ring: 217 94 42;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
THEME CONFIGURATION
|
||||
Typography: Source Serif Pro (primary) + Aspekta (secondary/UI)
|
||||
Typography: Source Serif Pro (primary/serif) + Aspekta (secondary/UI/sans)
|
||||
============================================================================= */
|
||||
|
||||
@theme inline {
|
||||
/* Typography - Using CSS variables from Next.js font loading */
|
||||
--font-sans: var(--font-inter), 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-serif: var(--font-source-serif), 'Source Serif 4', 'Source Serif Pro', Georgia, 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;
|
||||
|
||||
/* Color mappings */
|
||||
|
||||
/* Color mappings — maps CSS var RGB triplets to Tailwind utility classes */
|
||||
--color-background: rgb(var(--background));
|
||||
--color-foreground: rgb(var(--foreground));
|
||||
--color-card: rgb(var(--card));
|
||||
@@ -178,6 +75,7 @@
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 2px);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-hero-bg: rgb(var(--hero-bg));
|
||||
--color-sidebar: rgb(var(--sidebar));
|
||||
--color-sidebar-foreground: rgb(var(--sidebar-foreground));
|
||||
--color-sidebar-primary: rgb(var(--sidebar-primary));
|
||||
@@ -186,7 +84,7 @@
|
||||
--color-sidebar-accent-foreground: rgb(var(--sidebar-accent-foreground));
|
||||
--color-sidebar-border: rgb(var(--sidebar-border));
|
||||
--color-sidebar-ring: rgb(var(--sidebar-ring));
|
||||
|
||||
|
||||
/* Greyhaven-specific colors for direct use */
|
||||
--color-greyhaven-orange: #D95E2A;
|
||||
--color-greyhaven-offblack: #161614;
|
||||
|
||||
@@ -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 (
|
||||
<html lang="en" className={`${sourceSerif.variable} ${inter.variable}`}>
|
||||
<html lang="en" className={sourceSerif.variable}>
|
||||
<body className="font-sans antialiased bg-background text-foreground">
|
||||
{children}
|
||||
</body>
|
||||
|
||||
170
app/tokens/TOKENS.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Greyhaven Design Tokens Reference
|
||||
|
||||
> Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
> Source: `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` | `#f0f0ec` | Page background |
|
||||
| `color.semantic.foreground` | `#161614` | Primary text |
|
||||
| `color.semantic.card` | `#f9f9f7` | Card/elevated surface background |
|
||||
| `color.semantic.card-foreground` | `#161614` | Card text |
|
||||
| `color.semantic.popover` | `#f9f9f7` | Popover background |
|
||||
| `color.semantic.popover-foreground` | `#161614` | Popover text |
|
||||
| `color.semantic.primary` | `#d95e2a` | Primary accent — buttons, links, focus rings |
|
||||
| `color.semantic.primary-foreground` | `#f9f9f7` | Text on primary accent |
|
||||
| `color.semantic.secondary` | `#f0f0ec` | Secondary button/surface |
|
||||
| `color.semantic.secondary-foreground` | `#2f2f2c` | Text on secondary surface |
|
||||
| `color.semantic.muted` | `#f0f0ec` | Muted/subdued background |
|
||||
| `color.semantic.muted-foreground` | `#575753` | Muted/subdued text |
|
||||
| `color.semantic.accent` | `#ddddd7` | Subtle hover state |
|
||||
| `color.semantic.accent-foreground` | `#161614` | Text on accent hover |
|
||||
| `color.semantic.destructive` | `#b43232` | Destructive/error actions |
|
||||
| `color.semantic.destructive-foreground` | `#f9f9f7` | Text on destructive |
|
||||
| `color.semantic.border` | `#c4c4bd` | Default border |
|
||||
| `color.semantic.input` | `#c4c4bd` | Input border |
|
||||
| `color.semantic.ring` | `#d95e2a` | Focus ring |
|
||||
| `color.semantic.hero-bg` | `#ddddd7` | Hero banner background |
|
||||
| `color.semantic.chart.1` | `#d95e2a` | Chart accent |
|
||||
| `color.semantic.chart.2` | `#575753` | Chart secondary |
|
||||
| `color.semantic.chart.3` | `#7f7f79` | Chart tertiary |
|
||||
| `color.semantic.chart.4` | `#a6a69f` | Chart quaternary |
|
||||
| `color.semantic.chart.5` | `#2f2f2c` | Chart quinary |
|
||||
| `color.semantic.sidebar.background` | `#f0f0ec` | Sidebar background |
|
||||
| `color.semantic.sidebar.foreground` | `#161614` | Sidebar text |
|
||||
| `color.semantic.sidebar.primary` | `#d95e2a` | Sidebar primary accent |
|
||||
| `color.semantic.sidebar.primary-foreground` | `#f9f9f7` | Sidebar primary text |
|
||||
| `color.semantic.sidebar.accent` | `#c4c4bd` | Sidebar accent/hover |
|
||||
| `color.semantic.sidebar.accent-foreground` | `#161614` | Sidebar accent text |
|
||||
| `color.semantic.sidebar.border` | `#c4c4bd` | Sidebar border |
|
||||
| `color.semantic.sidebar.ring` | `#d95e2a` | Sidebar focus ring |
|
||||
| `color.dark.background` | `#161614` | Dark page background |
|
||||
| `color.dark.foreground` | `#f9f9f7` | Dark primary text |
|
||||
| `color.dark.card` | `#2f2f2c` | Dark card surface |
|
||||
| `color.dark.card-foreground` | `#f9f9f7` | Dark card text |
|
||||
| `color.dark.popover` | `#2f2f2c` | Dark popover |
|
||||
| `color.dark.popover-foreground` | `#f9f9f7` | Dark popover text |
|
||||
| `color.dark.primary` | `#d95e2a` | Same orange in dark mode |
|
||||
| `color.dark.primary-foreground` | `#f9f9f7` | Dark primary foreground |
|
||||
| `color.dark.secondary` | `#575753` | Dark secondary |
|
||||
| `color.dark.secondary-foreground` | `#f9f9f7` | Dark secondary text |
|
||||
| `color.dark.muted` | `#2f2f2c` | Dark muted — grey.8 (distinct from grey.7 border so outlines on bg-muted surfaces remain visible) |
|
||||
| `color.dark.muted-foreground` | `#c4c4bd` | Dark muted text |
|
||||
| `color.dark.accent` | `#575753` | Dark accent/hover |
|
||||
| `color.dark.accent-foreground` | `#f9f9f7` | Dark accent text |
|
||||
| `color.dark.destructive` | `#b43232` | Same destructive in dark mode |
|
||||
| `color.dark.destructive-foreground` | `#f9f9f7` | Dark destructive text |
|
||||
| `color.dark.border` | `#575753` | Dark border |
|
||||
| `color.dark.input` | `#575753` | Dark input border |
|
||||
| `color.dark.ring` | `#d95e2a` | Dark focus ring |
|
||||
| `color.dark.hero-bg` | `#2f2f2c` | Dark hero banner background |
|
||||
| `color.dark.chart.1` | `#d95e2a` | Dark chart accent |
|
||||
| `color.dark.chart.2` | `#c4c4bd` | Dark chart secondary |
|
||||
| `color.dark.chart.3` | `#a6a69f` | Dark chart tertiary |
|
||||
| `color.dark.chart.4` | `#7f7f79` | Dark chart quaternary |
|
||||
| `color.dark.chart.5` | `#f0f0ec` | Dark chart quinary |
|
||||
| `color.dark.sidebar.background` | `#2f2f2c` | Dark sidebar background |
|
||||
| `color.dark.sidebar.foreground` | `#f9f9f7` | Dark sidebar text |
|
||||
| `color.dark.sidebar.primary` | `#d95e2a` | Dark sidebar primary |
|
||||
| `color.dark.sidebar.primary-foreground` | `#f9f9f7` | Dark sidebar primary text |
|
||||
| `color.dark.sidebar.accent` | `#575753` | Dark sidebar accent |
|
||||
| `color.dark.sidebar.accent-foreground` | `#f9f9f7` | Dark sidebar accent text |
|
||||
| `color.dark.sidebar.border` | `#575753` | Dark sidebar border |
|
||||
| `color.dark.sidebar.ring` | `#d95e2a` | Dark sidebar ring |
|
||||
|
||||
## 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` | `cubic-bezier(0.4, 0, 0.2, 1)` | Standard ease-in-out |
|
||||
| `motion.easing.in` | `cubic-bezier(0.4, 0, 1, 1)` | Ease-in for exits |
|
||||
| `motion.easing.out` | `cubic-bezier(0, 0, 0.2, 1)` | Ease-out for entrances |
|
||||
|
||||
## 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) |
|
||||
|
||||
## Shadow
|
||||
|
||||
| Token | Value | Description |
|
||||
|-------|-------|-------------|
|
||||
| `shadow.xs` | `0 1px 2px 0 rgba(22, 22, 20, 0.05)` | Subtle shadow for buttons, inputs |
|
||||
| `shadow.sm` | `0 1px 3px 0 rgba(22, 22, 20, 0.1)` | Small shadow for cards |
|
||||
| `shadow.md` | `0 4px 6px -1px rgba(22, 22, 20, 0.1)` | Medium shadow for dropdowns, popovers |
|
||||
| `shadow.lg` | `0 10px 15px -3px rgba(22, 22, 20, 0.1)` | Large shadow for dialogs, modals |
|
||||
|
||||
## 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) |
|
||||
|
||||
## 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.25rem` | Headings |
|
||||
| `typography.lineHeight.normal` | `1.5rem` | Default |
|
||||
| `typography.lineHeight.relaxed` | `1.625rem` | 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 |
|
||||
72
app/tokens/tokens-dark.css
Normal file
@@ -0,0 +1,72 @@
|
||||
/* Greyhaven Design Tokens — Dark Theme
|
||||
Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
Source: tokens/color.json */
|
||||
|
||||
.dark {
|
||||
/* Dark page background */
|
||||
--background: 22 22 20;
|
||||
/* Dark primary text */
|
||||
--foreground: 249 249 247;
|
||||
/* Dark card surface */
|
||||
--card: 47 47 44;
|
||||
/* Dark card text */
|
||||
--card-foreground: 249 249 247;
|
||||
/* Dark popover */
|
||||
--popover: 47 47 44;
|
||||
/* Dark popover text */
|
||||
--popover-foreground: 249 249 247;
|
||||
/* Same orange in dark mode */
|
||||
--primary: 217 94 42;
|
||||
/* Dark primary foreground */
|
||||
--primary-foreground: 249 249 247;
|
||||
/* Dark secondary */
|
||||
--secondary: 87 87 83;
|
||||
/* Dark secondary text */
|
||||
--secondary-foreground: 249 249 247;
|
||||
/* Dark muted — grey.8 (distinct from grey.7 border so outlines on bg-muted surfaces remain visible) */
|
||||
--muted: 47 47 44;
|
||||
/* Dark muted text */
|
||||
--muted-foreground: 196 196 189;
|
||||
/* Dark accent/hover */
|
||||
--accent: 87 87 83;
|
||||
/* Dark accent text */
|
||||
--accent-foreground: 249 249 247;
|
||||
/* Same destructive in dark mode */
|
||||
--destructive: 180 50 50;
|
||||
/* Dark destructive text */
|
||||
--destructive-foreground: 249 249 247;
|
||||
/* Dark border */
|
||||
--border: 87 87 83;
|
||||
/* Dark input border */
|
||||
--input: 87 87 83;
|
||||
/* Dark focus ring */
|
||||
--ring: 217 94 42;
|
||||
/* Dark hero banner background */
|
||||
--hero-bg: 47 47 44;
|
||||
/* Dark chart accent */
|
||||
--chart-1: 217 94 42;
|
||||
/* Dark chart secondary */
|
||||
--chart-2: 196 196 189;
|
||||
/* Dark chart tertiary */
|
||||
--chart-3: 166 166 159;
|
||||
/* Dark chart quaternary */
|
||||
--chart-4: 127 127 121;
|
||||
/* Dark chart quinary */
|
||||
--chart-5: 240 240 236;
|
||||
/* Dark sidebar background */
|
||||
--sidebar: 47 47 44;
|
||||
/* Dark sidebar text */
|
||||
--sidebar-foreground: 249 249 247;
|
||||
/* Dark sidebar primary */
|
||||
--sidebar-primary: 217 94 42;
|
||||
/* Dark sidebar primary text */
|
||||
--sidebar-primary-foreground: 249 249 247;
|
||||
/* Dark sidebar accent */
|
||||
--sidebar-accent: 87 87 83;
|
||||
/* Dark sidebar accent text */
|
||||
--sidebar-accent-foreground: 249 249 247;
|
||||
/* Dark sidebar border */
|
||||
--sidebar-border: 87 87 83;
|
||||
/* Dark sidebar ring */
|
||||
--sidebar-ring: 217 94 42;
|
||||
}
|
||||
72
app/tokens/tokens-light.css
Normal file
@@ -0,0 +1,72 @@
|
||||
/* Greyhaven Design Tokens — Light Theme
|
||||
Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
Source: tokens/color.json */
|
||||
|
||||
:root {
|
||||
/* Page background */
|
||||
--background: 240 240 236;
|
||||
/* Primary text */
|
||||
--foreground: 22 22 20;
|
||||
/* Card/elevated surface background */
|
||||
--card: 249 249 247;
|
||||
/* Card text */
|
||||
--card-foreground: 22 22 20;
|
||||
/* Popover background */
|
||||
--popover: 249 249 247;
|
||||
/* Popover text */
|
||||
--popover-foreground: 22 22 20;
|
||||
/* Primary accent — buttons, links, focus rings */
|
||||
--primary: 217 94 42;
|
||||
/* Text on primary accent */
|
||||
--primary-foreground: 249 249 247;
|
||||
/* Secondary button/surface */
|
||||
--secondary: 240 240 236;
|
||||
/* Text on secondary surface */
|
||||
--secondary-foreground: 47 47 44;
|
||||
/* Muted/subdued background */
|
||||
--muted: 240 240 236;
|
||||
/* Muted/subdued text */
|
||||
--muted-foreground: 87 87 83;
|
||||
/* Subtle hover state */
|
||||
--accent: 221 221 215;
|
||||
/* Text on accent hover */
|
||||
--accent-foreground: 22 22 20;
|
||||
/* Destructive/error actions */
|
||||
--destructive: 180 50 50;
|
||||
/* Text on destructive */
|
||||
--destructive-foreground: 249 249 247;
|
||||
/* Default border */
|
||||
--border: 196 196 189;
|
||||
/* Input border */
|
||||
--input: 196 196 189;
|
||||
/* Focus ring */
|
||||
--ring: 217 94 42;
|
||||
/* Hero banner background */
|
||||
--hero-bg: 221 221 215;
|
||||
/* Chart accent */
|
||||
--chart-1: 217 94 42;
|
||||
/* Chart secondary */
|
||||
--chart-2: 87 87 83;
|
||||
/* Chart tertiary */
|
||||
--chart-3: 127 127 121;
|
||||
/* Chart quaternary */
|
||||
--chart-4: 166 166 159;
|
||||
/* Chart quinary */
|
||||
--chart-5: 47 47 44;
|
||||
/* Sidebar background */
|
||||
--sidebar: 240 240 236;
|
||||
/* Sidebar text */
|
||||
--sidebar-foreground: 22 22 20;
|
||||
/* Sidebar primary accent */
|
||||
--sidebar-primary: 217 94 42;
|
||||
/* Sidebar primary text */
|
||||
--sidebar-primary-foreground: 249 249 247;
|
||||
/* Sidebar accent/hover */
|
||||
--sidebar-accent: 196 196 189;
|
||||
/* Sidebar accent text */
|
||||
--sidebar-accent-foreground: 22 22 20;
|
||||
/* Sidebar border */
|
||||
--sidebar-border: 196 196 189;
|
||||
/* Sidebar focus ring */
|
||||
--sidebar-ring: 217 94 42;
|
||||
}
|
||||
146
app/tokens/tokens.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
// Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
// Source: tokens/*.json (W3C DTCG format)
|
||||
|
||||
export const ColorTokens = {
|
||||
'primitive.off-white': '#f9f9f7',
|
||||
'primitive.off-black': '#161614',
|
||||
'primitive.orange': '#d95e2a',
|
||||
'primitive.destructive-red': '#b43232',
|
||||
'primitive.grey.1': '#f0f0ec',
|
||||
'primitive.grey.2': '#ddddd7',
|
||||
'primitive.grey.3': '#c4c4bd',
|
||||
'primitive.grey.4': '#a6a69f',
|
||||
'primitive.grey.5': '#7f7f79',
|
||||
'primitive.grey.7': '#575753',
|
||||
'primitive.grey.8': '#2f2f2c',
|
||||
'semantic.background': '#f0f0ec',
|
||||
'semantic.foreground': '#161614',
|
||||
'semantic.card': '#f9f9f7',
|
||||
'semantic.card-foreground': '#161614',
|
||||
'semantic.popover': '#f9f9f7',
|
||||
'semantic.popover-foreground': '#161614',
|
||||
'semantic.primary': '#d95e2a',
|
||||
'semantic.primary-foreground': '#f9f9f7',
|
||||
'semantic.secondary': '#f0f0ec',
|
||||
'semantic.secondary-foreground': '#2f2f2c',
|
||||
'semantic.muted': '#f0f0ec',
|
||||
'semantic.muted-foreground': '#575753',
|
||||
'semantic.accent': '#ddddd7',
|
||||
'semantic.accent-foreground': '#161614',
|
||||
'semantic.destructive': '#b43232',
|
||||
'semantic.destructive-foreground': '#f9f9f7',
|
||||
'semantic.border': '#c4c4bd',
|
||||
'semantic.input': '#c4c4bd',
|
||||
'semantic.ring': '#d95e2a',
|
||||
'semantic.hero-bg': '#ddddd7',
|
||||
'semantic.chart.1': '#d95e2a',
|
||||
'semantic.chart.2': '#575753',
|
||||
'semantic.chart.3': '#7f7f79',
|
||||
'semantic.chart.4': '#a6a69f',
|
||||
'semantic.chart.5': '#2f2f2c',
|
||||
'semantic.sidebar.background': '#f0f0ec',
|
||||
'semantic.sidebar.foreground': '#161614',
|
||||
'semantic.sidebar.primary': '#d95e2a',
|
||||
'semantic.sidebar.primary-foreground': '#f9f9f7',
|
||||
'semantic.sidebar.accent': '#c4c4bd',
|
||||
'semantic.sidebar.accent-foreground': '#161614',
|
||||
'semantic.sidebar.border': '#c4c4bd',
|
||||
'semantic.sidebar.ring': '#d95e2a',
|
||||
'dark.background': '#161614',
|
||||
'dark.foreground': '#f9f9f7',
|
||||
'dark.card': '#2f2f2c',
|
||||
'dark.card-foreground': '#f9f9f7',
|
||||
'dark.popover': '#2f2f2c',
|
||||
'dark.popover-foreground': '#f9f9f7',
|
||||
'dark.primary': '#d95e2a',
|
||||
'dark.primary-foreground': '#f9f9f7',
|
||||
'dark.secondary': '#575753',
|
||||
'dark.secondary-foreground': '#f9f9f7',
|
||||
'dark.muted': '#2f2f2c',
|
||||
'dark.muted-foreground': '#c4c4bd',
|
||||
'dark.accent': '#575753',
|
||||
'dark.accent-foreground': '#f9f9f7',
|
||||
'dark.destructive': '#b43232',
|
||||
'dark.destructive-foreground': '#f9f9f7',
|
||||
'dark.border': '#575753',
|
||||
'dark.input': '#575753',
|
||||
'dark.ring': '#d95e2a',
|
||||
'dark.hero-bg': '#2f2f2c',
|
||||
'dark.chart.1': '#d95e2a',
|
||||
'dark.chart.2': '#c4c4bd',
|
||||
'dark.chart.3': '#a6a69f',
|
||||
'dark.chart.4': '#7f7f79',
|
||||
'dark.chart.5': '#f0f0ec',
|
||||
'dark.sidebar.background': '#2f2f2c',
|
||||
'dark.sidebar.foreground': '#f9f9f7',
|
||||
'dark.sidebar.primary': '#d95e2a',
|
||||
'dark.sidebar.primary-foreground': '#f9f9f7',
|
||||
'dark.sidebar.accent': '#575753',
|
||||
'dark.sidebar.accent-foreground': '#f9f9f7',
|
||||
'dark.sidebar.border': '#575753',
|
||||
'dark.sidebar.ring': '#d95e2a',
|
||||
} as const;
|
||||
|
||||
export const MotionTokens = {
|
||||
'duration.fast': '150ms',
|
||||
'duration.normal': '200ms',
|
||||
'duration.slow': '300ms',
|
||||
} as const;
|
||||
|
||||
export const RadiiTokens = {
|
||||
'base': '0.375rem',
|
||||
'sm': 'calc(0.375rem - 2px)',
|
||||
'md': '0.375rem',
|
||||
'lg': 'calc(0.375rem + 2px)',
|
||||
'xl': 'calc(0.375rem + 4px)',
|
||||
'full': '9999px',
|
||||
} as const;
|
||||
|
||||
export const ShadowTokens = {
|
||||
} as const;
|
||||
|
||||
export const SpacingTokens = {
|
||||
'0': '0',
|
||||
'1': '0.25rem',
|
||||
'2': '0.5rem',
|
||||
'3': '0.75rem',
|
||||
'4': '1rem',
|
||||
'5': '1.25rem',
|
||||
'6': '1.5rem',
|
||||
'8': '2rem',
|
||||
'10': '2.5rem',
|
||||
'12': '3rem',
|
||||
'16': '4rem',
|
||||
'20': '5rem',
|
||||
'24': '6rem',
|
||||
'0.5': '0.125rem',
|
||||
'1.5': '0.375rem',
|
||||
'component.card-padding': '1.5rem',
|
||||
'component.card-gap': '1.5rem',
|
||||
'component.section-padding': '2.5rem',
|
||||
'component.form-gap': '1rem',
|
||||
'component.button-padding-x': '1rem',
|
||||
'component.navbar-height': '4rem',
|
||||
} as const;
|
||||
|
||||
export const TypographyTokens = {
|
||||
'fontSize.xs': '0.75rem',
|
||||
'fontSize.sm': '0.875rem',
|
||||
'fontSize.base': '1rem',
|
||||
'fontSize.lg': '1.125rem',
|
||||
'fontSize.xl': '1.25rem',
|
||||
'fontSize.2xl': '1.5rem',
|
||||
'fontSize.3xl': '1.875rem',
|
||||
'fontSize.4xl': '2.25rem',
|
||||
'fontSize.5xl': '3rem',
|
||||
'fontWeight.normal': '400',
|
||||
'fontWeight.medium': '500',
|
||||
'fontWeight.semibold': '600',
|
||||
'fontWeight.bold': '700',
|
||||
'lineHeight.tight': '1.25rem',
|
||||
'lineHeight.normal': '1.5rem',
|
||||
'lineHeight.relaxed': '1.625rem',
|
||||
'letterSpacing.tight': '-0.025em',
|
||||
'letterSpacing.normal': '0em',
|
||||
'letterSpacing.wide': '0.05em',
|
||||
} as const;
|
||||
@@ -1,11 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ThemeProvider as NextThemesProvider,
|
||||
type ThemeProviderProps,
|
||||
} from 'next-themes'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
attribute?: string
|
||||
}
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme
|
||||
resolvedTheme: 'light' | 'dark'
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const ThemeContext = React.createContext<ThemeContextValue | undefined>(undefined)
|
||||
|
||||
function getSystemTheme(): 'light' | 'dark' {
|
||||
if (typeof window === 'undefined') return 'light'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = 'system',
|
||||
storageKey = 'greyhaven-theme',
|
||||
attribute = 'class',
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setThemeState] = React.useState<Theme>(() => {
|
||||
if (typeof window === 'undefined') return defaultTheme
|
||||
return (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
})
|
||||
|
||||
const resolvedTheme = theme === 'system' ? getSystemTheme() : theme
|
||||
|
||||
const setTheme = React.useCallback(
|
||||
(newTheme: Theme) => {
|
||||
setThemeState(newTheme)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(storageKey, newTheme)
|
||||
}
|
||||
},
|
||||
[storageKey],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const root = document.documentElement
|
||||
|
||||
if (attribute === 'class') {
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(resolvedTheme)
|
||||
} else {
|
||||
root.setAttribute(attribute, resolvedTheme)
|
||||
}
|
||||
}, [resolvedTheme, attribute])
|
||||
|
||||
// Listen for system theme changes when theme is 'system'
|
||||
React.useEffect(() => {
|
||||
if (theme !== 'system') return
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handler = () => setThemeState((prev) => (prev === 'system' ? 'system' : prev))
|
||||
|
||||
mediaQuery.addEventListener('change', handler)
|
||||
return () => mediaQuery.removeEventListener('change', handler)
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = React.useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
'inline-flex items-center justify-center rounded-md border font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -43,9 +43,15 @@ const badgeVariants = cva(
|
||||
platform:
|
||||
'border-transparent bg-[#f97316] text-white [a&]:hover:bg-[#f97316]/90',
|
||||
},
|
||||
size: {
|
||||
sm: 'text-xs px-1.5 py-0',
|
||||
default: 'text-xs px-2 py-0.5',
|
||||
lg: 'text-sm px-3 py-1 [&>svg]:size-3.5',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -53,6 +59,7 @@ const badgeVariants = cva(
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> &
|
||||
@@ -62,7 +69,7 @@ function Badge({
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
className={cn(badgeVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
63
components/ui/code.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const codeVariants = cva(
|
||||
'bg-muted border border-border font-mono text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
inline: 'rounded text-xs px-1.5 py-0.5',
|
||||
block: 'block rounded-md text-sm px-4 py-3 leading-relaxed break-all whitespace-pre-wrap',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'inline',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
interface CodeProps
|
||||
extends React.ComponentProps<'code'>,
|
||||
VariantProps<typeof codeVariants> {
|
||||
/**
|
||||
* Optional language hint for future syntax-highlighting support.
|
||||
* Emitted as `data-language` and as a `language-{lang}` class so
|
||||
* highlighters like Prism/Shiki can pick it up later.
|
||||
*/
|
||||
language?: string
|
||||
}
|
||||
|
||||
function Code({
|
||||
className,
|
||||
variant,
|
||||
language,
|
||||
...props
|
||||
}: CodeProps) {
|
||||
const element = (
|
||||
<code
|
||||
data-slot="code"
|
||||
data-language={language}
|
||||
className={cn(
|
||||
codeVariants({ variant, className }),
|
||||
language && `language-${language}`,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
// For block variant, wrap in <pre> so copy-paste preserves whitespace
|
||||
// and screen readers announce it as a code block.
|
||||
if (variant === 'block') {
|
||||
return (
|
||||
<pre data-slot="code-block" className="not-prose">
|
||||
{element}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
export { Code, codeVariants }
|
||||
86
components/ui/cta-section.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const ctaSectionVariants = cva('py-16 px-6', {
|
||||
variants: {
|
||||
variant: {
|
||||
centered: 'text-center',
|
||||
'left-aligned': 'text-left',
|
||||
},
|
||||
background: {
|
||||
default: 'bg-background',
|
||||
muted: 'bg-muted',
|
||||
accent: 'bg-primary text-primary-foreground',
|
||||
subtle: 'bg-primary/5',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'centered',
|
||||
background: 'muted',
|
||||
},
|
||||
})
|
||||
|
||||
interface CTASectionProps
|
||||
extends React.ComponentProps<'section'>,
|
||||
VariantProps<typeof ctaSectionVariants> {
|
||||
heading: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
actions?: React.ReactNode
|
||||
}
|
||||
|
||||
function CTASection({
|
||||
className,
|
||||
variant,
|
||||
background,
|
||||
heading,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
...props
|
||||
}: CTASectionProps) {
|
||||
return (
|
||||
<section
|
||||
data-slot="cta-section"
|
||||
className={cn(ctaSectionVariants({ variant, background, className }))}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-3xl',
|
||||
variant === 'centered' && 'mx-auto',
|
||||
)}
|
||||
>
|
||||
<h2 className="font-serif text-2xl sm:text-3xl font-semibold tracking-tight mb-4">
|
||||
{heading}
|
||||
</h2>
|
||||
{description && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-base font-sans mb-8 leading-relaxed',
|
||||
background === 'accent'
|
||||
? 'text-primary-foreground/80'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{actions && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-wrap gap-4',
|
||||
variant === 'centered' && 'justify-center',
|
||||
)}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { CTASection, ctaSectionVariants }
|
||||
109
components/ui/footer.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const footerVariants = cva(
|
||||
'border-t border-border bg-background font-sans',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
minimal: 'py-8',
|
||||
full: 'py-12',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'minimal',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
interface FooterLinkGroup {
|
||||
title: string
|
||||
links: { label: string; href: string }[]
|
||||
}
|
||||
|
||||
interface FooterProps
|
||||
extends React.ComponentProps<'footer'>,
|
||||
VariantProps<typeof footerVariants> {
|
||||
logo?: React.ReactNode
|
||||
copyright?: React.ReactNode
|
||||
linkGroups?: FooterLinkGroup[]
|
||||
actions?: React.ReactNode
|
||||
}
|
||||
|
||||
function Footer({
|
||||
className,
|
||||
variant,
|
||||
logo,
|
||||
copyright,
|
||||
linkGroups,
|
||||
actions,
|
||||
children,
|
||||
...props
|
||||
}: FooterProps) {
|
||||
if (variant === 'full' && linkGroups) {
|
||||
return (
|
||||
<footer
|
||||
data-slot="footer"
|
||||
className={cn(footerVariants({ variant, className }))}
|
||||
{...props}
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8">
|
||||
{logo && (
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
{logo}
|
||||
</div>
|
||||
)}
|
||||
{linkGroups.map((group) => (
|
||||
<div key={group.title}>
|
||||
<h4 className="text-sm font-semibold mb-4">{group.title}</h4>
|
||||
<ul className="space-y-2">
|
||||
{group.links.map((link) => (
|
||||
<li key={link.href}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-border pt-8 flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{copyright && (
|
||||
<p className="text-sm text-muted-foreground">{copyright}</p>
|
||||
)}
|
||||
{actions}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<footer
|
||||
data-slot="footer"
|
||||
className={cn(footerVariants({ variant, className }))}
|
||||
{...props}
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{logo && <div>{logo}</div>}
|
||||
{copyright && (
|
||||
<p className="text-sm text-muted-foreground">{copyright}</p>
|
||||
)}
|
||||
{actions}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export { Footer, footerVariants }
|
||||
94
components/ui/hero.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const heroVariants = cva('py-24 px-6', {
|
||||
variants: {
|
||||
variant: {
|
||||
centered: 'text-center',
|
||||
'left-aligned': 'text-left',
|
||||
split: 'text-left',
|
||||
},
|
||||
background: {
|
||||
default: 'bg-hero-bg',
|
||||
muted: 'bg-muted',
|
||||
accent: 'bg-primary/5',
|
||||
dark: 'bg-foreground text-background',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'centered',
|
||||
background: 'default',
|
||||
},
|
||||
})
|
||||
|
||||
interface HeroProps
|
||||
extends React.ComponentProps<'section'>,
|
||||
VariantProps<typeof heroVariants> {
|
||||
heading: React.ReactNode
|
||||
subheading?: React.ReactNode
|
||||
actions?: React.ReactNode
|
||||
media?: React.ReactNode
|
||||
}
|
||||
|
||||
function Hero({
|
||||
className,
|
||||
variant,
|
||||
background,
|
||||
heading,
|
||||
subheading,
|
||||
actions,
|
||||
media,
|
||||
children,
|
||||
...props
|
||||
}: HeroProps) {
|
||||
const isSplit = variant === 'split'
|
||||
|
||||
return (
|
||||
<section
|
||||
data-slot="hero"
|
||||
className={cn(heroVariants({ variant, background, className }))}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-7xl mx-auto',
|
||||
isSplit && 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
variant === 'centered' && 'max-w-3xl mx-auto',
|
||||
!isSplit && 'max-w-3xl',
|
||||
)}
|
||||
>
|
||||
<h1 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight mb-6">
|
||||
{heading}
|
||||
</h1>
|
||||
{subheading && (
|
||||
<p className="text-lg text-muted-foreground font-sans mb-8 leading-relaxed">
|
||||
{subheading}
|
||||
</p>
|
||||
)}
|
||||
{actions && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-wrap gap-4',
|
||||
variant === 'centered' && 'justify-center',
|
||||
)}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
{isSplit && media && (
|
||||
<div className="flex items-center justify-center">{media}</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Hero, heroVariants }
|
||||
92
components/ui/logo.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const logoVariants = cva('inline-block', {
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-6 w-auto',
|
||||
md: 'h-8 w-auto',
|
||||
lg: 'h-10 w-auto',
|
||||
xl: 'h-14 w-auto',
|
||||
},
|
||||
variant: {
|
||||
color: '',
|
||||
monochrome: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
variant: 'color',
|
||||
},
|
||||
})
|
||||
|
||||
function Logo({
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<'svg'> &
|
||||
VariantProps<typeof logoVariants>) {
|
||||
return (
|
||||
<svg
|
||||
data-slot="logo"
|
||||
viewBox="0 0 1818 448"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn(logoVariants({ size, variant, className }))}
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#greyhaven-logo-clip)">
|
||||
<path
|
||||
d="M625.8 313.476L623.156 286.907C614.009 302.244 592.449 317.924 559.067 317.924C504.436 317.924 455.996 277.76 455.996 208.662C455.996 139.564 507.08 99.7111 561.4 99.7111C612.204 99.7111 644.684 128.956 655.884 163.489L622.502 176.182C615.409 152.569 594.751 132.471 561.369 132.471C527.987 132.471 491.991 156.676 491.991 208.662C491.991 260.649 525.062 285.444 561.089 285.444C603.307 285.444 619.267 256.511 621.04 238.498H551.942V207.48H654.422V313.476H625.769H625.8Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M771.649 203.622C767.822 203.031 763.964 202.751 760.418 202.751C733.849 202.751 721.747 218.089 721.747 244.969V313.476H687.493V169.68H720.876V192.702C727.658 177.053 743.618 167.907 762.502 167.907C766.64 167.907 770.187 168.498 771.649 168.778V203.622Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M919.582 272.44C911.898 297.547 889.156 317.924 854.622 317.924C815.64 317.924 781.107 289.582 781.107 240.862C781.107 195.378 814.769 165.262 851.076 165.262C895.378 165.262 921.356 194.507 921.356 239.96C921.356 245.56 920.764 250.289 920.453 250.88H815.329C816.2 272.72 833.342 288.369 854.591 288.369C875.84 288.369 885.889 277.449 890.618 263.262L919.551 272.409L919.582 272.44ZM886.822 225.773C886.231 208.942 875 193.884 851.387 193.884C829.827 193.884 817.444 210.404 816.262 225.773H886.822Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M949.542 371.653L984.107 296.364L922.693 169.68H961.365L1002.71 260.618L1041.38 169.68H1077.69L986.16 371.653H949.542Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M1128.09 313.476H1093.84V99.68H1128.09V183.556C1137.83 170.862 1154.07 165.542 1169.12 165.542C1204.56 165.542 1221.67 190.929 1221.67 222.538V313.476H1187.42V228.418C1187.42 210.684 1179.45 196.529 1157.89 196.529C1139.01 196.529 1128.65 210.684 1128.06 229.009V313.476H1128.09Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M1288.56 231.093L1325.46 225.493C1333.73 224.311 1336.1 220.173 1336.1 215.164C1336.1 203.062 1327.82 193.324 1308.94 193.324C1290.05 193.324 1280.88 204.836 1279.41 219.302L1248.12 212.209C1250.76 187.413 1273.22 165.262 1308.66 165.262C1352.96 165.262 1369.79 190.369 1369.79 218.991V290.453C1369.79 303.458 1371.28 312.013 1371.56 313.476H1339.68C1339.4 312.573 1338.21 306.693 1338.21 295.151C1331.43 306.071 1317.24 317.893 1293.91 317.893C1263.8 317.893 1245.19 297.236 1245.19 274.493C1245.19 248.796 1264.08 234.64 1288.59 231.093H1288.56ZM1336.1 253.836V247.333L1298.61 252.933C1287.97 254.707 1279.41 260.618 1279.41 272.44C1279.41 282.178 1286.79 291.044 1300.38 291.044C1319.58 291.044 1336.1 281.898 1336.1 253.836Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M1465.52 313.476H1431.27L1372.81 169.68H1410.61L1448.69 272.44L1485.9 169.68H1521.92L1465.52 313.476Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M1663.08 272.44C1655.39 297.547 1632.65 317.924 1598.12 317.924C1559.13 317.924 1524.6 289.582 1524.6 240.862C1524.6 195.378 1558.26 165.262 1594.57 165.262C1638.87 165.262 1664.85 194.507 1664.85 239.96C1664.85 245.56 1664.26 250.289 1663.95 250.88H1558.82C1559.69 272.72 1576.84 288.369 1598.08 288.369C1619.33 288.369 1629.38 277.449 1634.11 263.262L1663.04 272.409L1663.08 272.44ZM1630.28 225.773C1629.69 208.942 1618.46 193.884 1594.85 193.884C1573.29 193.884 1560.91 210.404 1559.72 225.773H1630.28Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M1724.12 313.476H1689.86V169.68H1723.24V188.876C1732.7 172.356 1749.81 165.542 1765.77 165.542C1800.9 165.542 1817.73 190.929 1817.73 222.538V313.476H1783.48V228.418C1783.48 210.684 1775.51 196.529 1753.95 196.529C1734.48 196.529 1724.12 211.587 1724.12 230.471V313.444V313.476Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-foreground'}
|
||||
/>
|
||||
<path
|
||||
d="M345.582 100.551L287.498 66.4533H284.356L232.462 96.9111V34.0978L174.378 0H171.236L113.151 34.0978V96.9111L61.2578 66.4533H58.1156L0 100.551V102.791V347.013L58.0844 381.609H61.2578L113.12 350.747V413.467L171.204 448.062H174.378L232.462 413.467V350.747L284.324 381.609H287.498L345.582 347.013V100.551ZM59.6711 72.7378L111.627 103.258L59.6711 134.182L7.71556 103.258L59.6711 72.7378ZM6.22222 109.604L56.56 139.564V308.436L6.22222 337.991V109.604ZM56.56 315.653V373.427L7.71556 344.338L56.56 315.653ZM62.7822 373.427V315.653L111.627 344.338L62.7822 373.427ZM113.12 337.991L62.7822 308.436V139.564L113.12 109.604V337.991ZM226.24 102.791V163.333L175.902 133.778V73.1422L226.24 43.1822V102.791ZM172.791 200.604L120.836 169.68L172.791 139.16L224.747 169.68L172.791 200.604ZM175.902 65.8933V8.12L224.747 36.8044L175.902 65.8933ZM169.68 8.12V65.8933L120.836 36.8044L169.68 8.12ZM119.342 43.1511L169.68 73.1111V133.747L119.342 163.302V43.1511ZM119.342 176.027L169.68 205.987V374.858L119.342 404.413V176.027ZM169.68 382.076V439.849L120.836 410.76L169.68 382.076ZM175.902 439.849V382.076L224.747 410.76L175.902 439.849ZM226.24 404.413L175.902 374.858V205.987L226.24 176.027V404.413ZM289.022 74.5733L337.867 103.258L289.022 132.347V74.5733ZM282.8 74.5733V132.347L233.956 103.258L282.8 74.5733ZM232.462 109.604L282.8 139.564V308.436L232.462 337.991V109.604ZM285.911 375.293L233.956 344.338L285.911 313.818L337.867 344.338L285.911 375.293ZM339.36 337.991L289.022 308.436V139.564L339.36 109.604V337.991Z"
|
||||
className={variant === 'monochrome' ? 'fill-foreground' : 'fill-primary'}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="greyhaven-logo-clip">
|
||||
<rect width="1817.73" height="448" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export { Logo, logoVariants }
|
||||
131
components/ui/navbar.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { MenuIcon, XIcon } from 'lucide-react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const navbarVariants = cva(
|
||||
'fixed top-0 left-0 right-0 z-50 h-[65px] font-sans',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
solid: 'bg-card dark:bg-background border-b border-border',
|
||||
transparent: 'bg-transparent',
|
||||
minimal: 'bg-card/80 dark:bg-background/80 backdrop-blur-sm border-b border-border/50',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'solid',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
interface NavbarProps
|
||||
extends React.ComponentProps<'header'>,
|
||||
VariantProps<typeof navbarVariants> {
|
||||
logo?: React.ReactNode
|
||||
actions?: React.ReactNode
|
||||
}
|
||||
|
||||
function Navbar({
|
||||
className,
|
||||
variant,
|
||||
logo,
|
||||
actions,
|
||||
children,
|
||||
...props
|
||||
}: NavbarProps) {
|
||||
const [mobileOpen, setMobileOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<header
|
||||
data-slot="navbar"
|
||||
className={cn(navbarVariants({ variant, className }))}
|
||||
{...props}
|
||||
>
|
||||
<div className="container mx-auto px-6 h-full flex items-center justify-between">
|
||||
{/* Logo slot — left */}
|
||||
{logo && (
|
||||
<div data-slot="navbar-logo" className="shrink-0">
|
||||
{logo}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop nav — center */}
|
||||
<nav
|
||||
data-slot="navbar-nav"
|
||||
className="hidden md:flex items-center gap-1 text-sm font-semibold"
|
||||
>
|
||||
{children}
|
||||
</nav>
|
||||
|
||||
{/* Actions slot — right */}
|
||||
<div className="flex items-center gap-2">
|
||||
{actions && (
|
||||
<div
|
||||
data-slot="navbar-actions"
|
||||
className="hidden md:flex items-center gap-2"
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile menu toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileOpen}
|
||||
>
|
||||
{mobileOpen ? (
|
||||
<XIcon className="size-5" />
|
||||
) : (
|
||||
<MenuIcon className="size-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile nav */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
data-slot="navbar-mobile"
|
||||
className="md:hidden border-b border-border bg-card dark:bg-background"
|
||||
>
|
||||
<nav className="container mx-auto px-6 py-4 flex flex-col gap-2 text-sm font-semibold">
|
||||
{children}
|
||||
</nav>
|
||||
{actions && (
|
||||
<div className="container mx-auto px-6 pb-4 flex flex-col gap-2">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function NavbarLink({
|
||||
className,
|
||||
active,
|
||||
...props
|
||||
}: React.ComponentProps<'a'> & { active?: boolean }) {
|
||||
return (
|
||||
<a
|
||||
data-slot="navbar-link"
|
||||
data-active={active || undefined}
|
||||
className={cn(
|
||||
'px-3 py-2 text-foreground transition-opacity hover:opacity-70',
|
||||
'data-active:text-primary data-active:opacity-100',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Navbar, NavbarLink, navbarVariants }
|
||||
52
components/ui/page-layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface PageLayoutProps extends React.ComponentProps<'div'> {
|
||||
navbar?: React.ReactNode
|
||||
sidebar?: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
}
|
||||
|
||||
function PageLayout({
|
||||
className,
|
||||
navbar,
|
||||
sidebar,
|
||||
footer,
|
||||
children,
|
||||
...props
|
||||
}: PageLayoutProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="page-layout"
|
||||
className={cn('min-h-screen flex flex-col bg-background text-foreground', className)}
|
||||
{...props}
|
||||
>
|
||||
{navbar}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1',
|
||||
navbar && 'pt-16.25', // offset for fixed 65px navbar
|
||||
)}
|
||||
>
|
||||
{sidebar && (
|
||||
<aside
|
||||
data-slot="page-layout-sidebar"
|
||||
className="hidden lg:block w-64 border-r border-border bg-background flex-shrink-0"
|
||||
>
|
||||
{sidebar}
|
||||
</aside>
|
||||
)}
|
||||
<main
|
||||
data-slot="page-layout-main"
|
||||
className="flex-1 min-w-0"
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { PageLayout }
|
||||
69
components/ui/section.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const sectionVariants = cva('py-10', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: '',
|
||||
highlighted: 'bg-card my-8',
|
||||
accent: 'bg-primary/5 my-8',
|
||||
},
|
||||
width: {
|
||||
narrow: 'max-w-3xl mx-auto',
|
||||
default: 'max-w-5xl mx-auto',
|
||||
wide: 'max-w-7xl mx-auto',
|
||||
full: 'w-full',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
width: 'default',
|
||||
},
|
||||
})
|
||||
|
||||
interface SectionProps
|
||||
extends React.ComponentProps<'section'>,
|
||||
VariantProps<typeof sectionVariants> {
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
function Section({
|
||||
className,
|
||||
variant,
|
||||
width,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
...props
|
||||
}: SectionProps) {
|
||||
return (
|
||||
<section
|
||||
data-slot="section"
|
||||
className={cn(sectionVariants({ variant, width, className }))}
|
||||
{...props}
|
||||
>
|
||||
<div className="px-6">
|
||||
{(title || description) && (
|
||||
<div className="mb-8">
|
||||
{title && (
|
||||
<h2 className="font-serif text-3xl font-semibold tracking-tight mb-3">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className="text-muted-foreground font-sans text-base max-w-2xl">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Section, sectionVariants }
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useTheme } from '@/components/theme-provider'
|
||||
import { Toaster as Sonner, ToasterProps } from 'sonner'
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = 'system' } = useTheme()
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
theme={resolvedTheme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
|
||||
439
lib/catalog.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* 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<string, unknown> {
|
||||
const filePath = path.join(root, 'tokens', `${name}.json`)
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
||||
}
|
||||
|
||||
export function flattenTokens(
|
||||
obj: Record<string, unknown>,
|
||||
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<string, unknown>
|
||||
|
||||
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: '<Button variant="default" size="default">Click me</Button>',
|
||||
},
|
||||
{
|
||||
name: 'Badge',
|
||||
file: 'components/ui/badge.tsx',
|
||||
category: 'primitives',
|
||||
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>\n<Badge variant="secondary" size="sm">3 items</Badge>\n<Badge variant="default" size="lg">New feature</Badge>',
|
||||
},
|
||||
{
|
||||
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: '<Input type="email" placeholder="you@example.com" />',
|
||||
},
|
||||
{
|
||||
name: 'Textarea',
|
||||
file: 'components/ui/textarea.tsx',
|
||||
category: 'primitives',
|
||||
exports: ['Textarea'],
|
||||
description: 'Multi-line text input.',
|
||||
props: 'All standard HTML textarea props',
|
||||
example: '<Textarea placeholder="Write your message..." />',
|
||||
},
|
||||
{
|
||||
name: 'Label',
|
||||
file: 'components/ui/label.tsx',
|
||||
category: 'primitives',
|
||||
exports: ['Label'],
|
||||
description: 'Form label using Radix Label primitive.',
|
||||
props: 'All standard HTML label props + Radix Label props',
|
||||
example: '<Label htmlFor="email">Email</Label>',
|
||||
},
|
||||
{
|
||||
name: 'Checkbox',
|
||||
file: 'components/ui/checkbox.tsx',
|
||||
category: 'primitives',
|
||||
exports: ['Checkbox'],
|
||||
description: 'Checkbox using Radix Checkbox primitive.',
|
||||
props: 'checked?: boolean; onCheckedChange?: (checked: boolean) => void',
|
||||
example: '<Checkbox id="terms" />',
|
||||
},
|
||||
{
|
||||
name: 'Switch',
|
||||
file: 'components/ui/switch.tsx',
|
||||
category: 'primitives',
|
||||
exports: ['Switch'],
|
||||
description: 'Toggle switch using Radix Switch primitive.',
|
||||
props: 'checked?: boolean; onCheckedChange?: (checked: boolean) => void',
|
||||
example: '<Switch id="dark-mode" />',
|
||||
},
|
||||
{
|
||||
name: 'Select',
|
||||
file: 'components/ui/select.tsx',
|
||||
category: 'primitives',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'RadioGroup',
|
||||
file: 'components/ui/radio-group.tsx',
|
||||
category: 'primitives',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Toggle',
|
||||
file: 'components/ui/toggle.tsx',
|
||||
category: 'primitives',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
file: 'components/ui/code.tsx',
|
||||
category: 'primitives',
|
||||
exports: ['Code', 'codeVariants'],
|
||||
description: 'Inline or block code snippet. Always use this instead of hand-rolling <code>/<pre> styling. Uses bg-muted + border-border so the outline stays visible in both light and dark modes. Block variant auto-wraps in <pre> 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>\n\n<Code variant="block" language="bash">{`pnpm install\npnpm dev`}</Code>',
|
||||
},
|
||||
// ── Layout ──────────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'Card',
|
||||
file: 'components/ui/card.tsx',
|
||||
category: 'layout',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Accordion',
|
||||
file: 'components/ui/accordion.tsx',
|
||||
category: 'layout',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Tabs',
|
||||
file: 'components/ui/tabs.tsx',
|
||||
category: 'layout',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Separator',
|
||||
file: 'components/ui/separator.tsx',
|
||||
category: 'layout',
|
||||
exports: ['Separator'],
|
||||
description: 'Visual divider line. Horizontal or vertical.',
|
||||
props: 'orientation?: "horizontal" | "vertical"; decorative?: boolean',
|
||||
example: '<Separator />',
|
||||
},
|
||||
// ── Overlay ─────────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'Dialog',
|
||||
file: 'components/ui/dialog.tsx',
|
||||
category: 'overlay',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'AlertDialog',
|
||||
file: 'components/ui/alert-dialog.tsx',
|
||||
category: 'overlay',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Tooltip',
|
||||
file: 'components/ui/tooltip.tsx',
|
||||
category: 'overlay',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Popover',
|
||||
file: 'components/ui/popover.tsx',
|
||||
category: 'overlay',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Drawer',
|
||||
file: 'components/ui/drawer.tsx',
|
||||
category: 'overlay',
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'Navbar',
|
||||
file: 'components/ui/navbar.tsx',
|
||||
category: 'navigation',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Breadcrumb',
|
||||
file: 'components/ui/breadcrumb.tsx',
|
||||
category: 'navigation',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Pagination',
|
||||
file: 'components/ui/pagination.tsx',
|
||||
category: 'navigation',
|
||||
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 Display ────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'Table',
|
||||
file: 'components/ui/table.tsx',
|
||||
category: 'data',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Progress',
|
||||
file: 'components/ui/progress.tsx',
|
||||
category: 'data',
|
||||
exports: ['Progress'],
|
||||
description: 'Progress bar using Radix Progress.',
|
||||
props: 'value?: number (0-100)',
|
||||
example: '<Progress value={60} />',
|
||||
},
|
||||
{
|
||||
name: 'Avatar',
|
||||
file: 'components/ui/avatar.tsx',
|
||||
category: 'data',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Calendar',
|
||||
file: 'components/ui/calendar.tsx',
|
||||
category: 'data',
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'Alert',
|
||||
file: 'components/ui/alert.tsx',
|
||||
category: 'feedback',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Skeleton',
|
||||
file: 'components/ui/skeleton.tsx',
|
||||
category: 'feedback',
|
||||
exports: ['Skeleton'],
|
||||
description: 'Loading placeholder with pulse animation.',
|
||||
props: 'Standard div props (set dimensions with className)',
|
||||
example: '<Skeleton className="h-4 w-[250px]" />',
|
||||
},
|
||||
{
|
||||
name: 'Spinner',
|
||||
file: 'components/ui/spinner.tsx',
|
||||
category: 'feedback',
|
||||
exports: ['Spinner'],
|
||||
description: 'Loading spinner (Loader2Icon with spin animation).',
|
||||
props: 'Standard SVG icon props',
|
||||
example: '<Spinner />',
|
||||
},
|
||||
{
|
||||
name: 'Empty',
|
||||
file: 'components/ui/empty.tsx',
|
||||
category: 'feedback',
|
||||
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 ────────────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'Form',
|
||||
file: 'components/ui/form.tsx',
|
||||
category: 'form',
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'Logo',
|
||||
file: 'components/ui/logo.tsx',
|
||||
category: 'composition',
|
||||
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" />',
|
||||
},
|
||||
{
|
||||
name: 'Hero',
|
||||
file: 'components/ui/hero.tsx',
|
||||
category: 'composition',
|
||||
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>} />',
|
||||
},
|
||||
{
|
||||
name: 'CTASection',
|
||||
file: 'components/ui/cta-section.tsx',
|
||||
category: 'composition',
|
||||
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>} />',
|
||||
},
|
||||
{
|
||||
name: 'Section',
|
||||
file: 'components/ui/section.tsx',
|
||||
category: 'composition',
|
||||
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>',
|
||||
},
|
||||
{
|
||||
name: 'Footer',
|
||||
file: 'components/ui/footer.tsx',
|
||||
category: 'composition',
|
||||
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" />',
|
||||
},
|
||||
{
|
||||
name: 'PageLayout',
|
||||
file: 'components/ui/page-layout.tsx',
|
||||
category: 'composition',
|
||||
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>',
|
||||
},
|
||||
]
|
||||
454
mcp/server.ts
Normal file
@@ -0,0 +1,454 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Greyhaven Design System MCP Server
|
||||
*
|
||||
* Provides programmatic access to design tokens, component specs, and
|
||||
* validation tools for any MCP-compatible AI agent.
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import { z } from 'zod'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import {
|
||||
COMPONENT_CATALOG,
|
||||
getTokens,
|
||||
type ComponentSpec,
|
||||
} from '../lib/catalog.js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT = path.resolve(__dirname, '..')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'greyhaven-design-system',
|
||||
version: '1.0.0',
|
||||
})
|
||||
|
||||
// Tool: get_tokens
|
||||
server.tool(
|
||||
'get_tokens',
|
||||
'Returns design token values. Optionally filter by category: color, typography, spacing, radii, shadows, motion.',
|
||||
{ category: z.string().optional().describe('Token category to filter by') },
|
||||
async ({ category }) => {
|
||||
const tokens = getTokens(ROOT, category)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify(tokens, null, 2),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Tool: get_component
|
||||
server.tool(
|
||||
'get_component',
|
||||
'Returns the full spec for a named component: props, variants, usage example, and when to use it.',
|
||||
{ name: z.string().describe('Component name (case-insensitive)') },
|
||||
async ({ name }) => {
|
||||
const component = COMPONENT_CATALOG.find(
|
||||
(c) => c.name.toLowerCase() === name.toLowerCase(),
|
||||
)
|
||||
if (!component) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `Component "${name}" not found. Use list_components to see available components.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
let source = ''
|
||||
try {
|
||||
source = fs.readFileSync(path.join(ROOT, component.file), 'utf-8')
|
||||
} catch {
|
||||
source = '(source file not readable)'
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ ...component, source }, null, 2),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Tool: list_components
|
||||
server.tool(
|
||||
'list_components',
|
||||
'Lists all available design system components, optionally filtered by category.',
|
||||
{
|
||||
category: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Category filter: primitives, layout, overlay, navigation, data, feedback, form, composition',
|
||||
),
|
||||
},
|
||||
async ({ category }) => {
|
||||
let components = COMPONENT_CATALOG
|
||||
if (category) {
|
||||
components = components.filter(
|
||||
(c) => c.category.toLowerCase() === category.toLowerCase(),
|
||||
)
|
||||
}
|
||||
|
||||
const summary = components.map((c) => ({
|
||||
name: c.name,
|
||||
category: c.category,
|
||||
file: c.file,
|
||||
description: c.description,
|
||||
}))
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify(summary, null, 2),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Tool: validate_colors
|
||||
server.tool(
|
||||
'validate_colors',
|
||||
'Checks if a code snippet uses valid design system colors. Returns warnings for raw hex values that should use tokens instead.',
|
||||
{ code: z.string().describe('Code string to validate') },
|
||||
async ({ code }) => {
|
||||
const hexRegex = /#[0-9a-fA-F]{3,8}/g
|
||||
const matches = code.match(hexRegex) || []
|
||||
|
||||
const validHexValues = new Set([
|
||||
'#f9f9f7', '#F9F9F7', '#161614', '#d95e2a', '#D95E2A',
|
||||
'#b43232', '#B43232', '#f0f0ec', '#F0F0EC', '#ddddd7', '#DDDDD7',
|
||||
'#c4c4bd', '#C4C4BD', '#a6a69f', '#A6A69F', '#7f7f79', '#7F7F79',
|
||||
'#575753', '#2f2f2c', '#2F2F2C',
|
||||
'#fff', '#FFF', '#ffffff', '#FFFFFF', '#000', '#000000',
|
||||
])
|
||||
|
||||
const warnings: string[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
for (const hex of matches) {
|
||||
const lower = hex.toLowerCase()
|
||||
if (seen.has(lower)) continue
|
||||
seen.add(lower)
|
||||
|
||||
if (validHexValues.has(hex)) {
|
||||
warnings.push(
|
||||
`${hex} -- valid Greyhaven primitive, but prefer semantic CSS variables (e.g., var(--primary)).`,
|
||||
)
|
||||
} else {
|
||||
warnings.push(
|
||||
`${hex} -- NOT a Greyhaven design token. Use semantic tokens: bg-primary, text-foreground, border-border, etc.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: 'No raw hex colors found. The code uses design system tokens correctly.',
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Found ${matches.length} hex color(s):\n\n${warnings.join('\n')}`,
|
||||
}],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Tool: suggest_component
|
||||
server.tool(
|
||||
'suggest_component',
|
||||
'Suggests the best Greyhaven component(s) for a described UI need.',
|
||||
{ description: z.string().describe('Natural language description of what UI you need') },
|
||||
async ({ description }) => {
|
||||
const desc = description.toLowerCase()
|
||||
const suggestions: ComponentSpec[] = []
|
||||
|
||||
const keywords: Record<string, string[]> = {
|
||||
button: ['button', 'click', 'action', 'submit', 'cta'],
|
||||
card: ['card', 'container', 'box', 'panel', 'tile'],
|
||||
dialog: ['dialog', 'modal', 'popup', 'overlay', 'confirm'],
|
||||
input: ['input', 'text field', 'textbox', 'form field'],
|
||||
table: ['table', 'grid', 'data', 'list', 'rows', 'columns'],
|
||||
navbar: ['navbar', 'navigation bar', 'header', 'top bar', 'nav'],
|
||||
hero: ['hero', 'banner', 'landing', 'splash', 'headline'],
|
||||
'cta-section': ['cta', 'call to action', 'signup', 'sign up'],
|
||||
footer: ['footer', 'bottom', 'copyright'],
|
||||
form: ['form', 'registration', 'login', 'sign in', 'contact'],
|
||||
select: ['select', 'dropdown', 'choose', 'pick'],
|
||||
tabs: ['tabs', 'tab', 'sections', 'switch between'],
|
||||
accordion: ['accordion', 'expandable', 'collapsible', 'faq'],
|
||||
alert: ['alert', 'warning', 'error', 'notification', 'message'],
|
||||
badge: ['badge', 'tag', 'label', 'status', 'chip'],
|
||||
avatar: ['avatar', 'profile', 'user image', 'photo'],
|
||||
tooltip: ['tooltip', 'hint', 'hover info'],
|
||||
progress: ['progress', 'loading', 'bar', 'percentage'],
|
||||
skeleton: ['skeleton', 'loading', 'placeholder', 'shimmer'],
|
||||
drawer: ['drawer', 'sheet', 'bottom sheet', 'slide'],
|
||||
popover: ['popover', 'floating', 'popup content'],
|
||||
separator: ['separator', 'divider', 'line', 'hr'],
|
||||
breadcrumb: ['breadcrumb', 'trail', 'path'],
|
||||
pagination: ['pagination', 'pages', 'next', 'previous'],
|
||||
section: ['section', 'content block', 'area'],
|
||||
'page-layout': ['layout', 'page', 'shell', 'scaffold', 'template'],
|
||||
logo: ['logo', 'brand', 'greyhaven'],
|
||||
calendar: ['calendar', 'date', 'date picker'],
|
||||
}
|
||||
|
||||
for (const [componentName, kw] of Object.entries(keywords)) {
|
||||
if (kw.some((k) => desc.includes(k))) {
|
||||
const match = COMPONENT_CATALOG.find(
|
||||
(c) => c.name.toLowerCase() === componentName.toLowerCase() ||
|
||||
c.name.toLowerCase().replace(/\s+/g, '-') === componentName,
|
||||
)
|
||||
if (match) suggestions.push(match)
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No strong match for "${description}". Use list_components() to browse all ${COMPONENT_CATALOG.length} components.`,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify(
|
||||
suggestions.map((s) => ({
|
||||
name: s.name,
|
||||
category: s.category,
|
||||
description: s.description,
|
||||
example: s.example,
|
||||
})),
|
||||
null, 2,
|
||||
),
|
||||
}],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brand tools & resources
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BRAND_SKILL_PATH = path.join(ROOT, 'skill', 'BRAND.md')
|
||||
|
||||
function readBrandSkill(): string {
|
||||
try {
|
||||
return fs.readFileSync(BRAND_SKILL_PATH, 'utf-8')
|
||||
} catch {
|
||||
return '(skill/BRAND.md not found — hand-curated brand skill is missing)'
|
||||
}
|
||||
}
|
||||
|
||||
server.tool(
|
||||
'get_brand_rules',
|
||||
'Returns the Greyhaven brand voice, tone, and messaging rules. Use this BEFORE generating any user-facing marketing copy, CTAs, landing page content, or product explanations. Covers positioning, brand axes, tone, writing rules, reasoning patterns, CTA guidance, logo usage, and a self-check list.',
|
||||
{
|
||||
section: z
|
||||
.enum([
|
||||
'all',
|
||||
'positioning',
|
||||
'axes',
|
||||
'tone',
|
||||
'writing-rules',
|
||||
'reasoning-patterns',
|
||||
'cta',
|
||||
'logo',
|
||||
'self-check',
|
||||
])
|
||||
.optional()
|
||||
.describe('Optional section filter. Default returns the full brand skill.'),
|
||||
},
|
||||
async ({ section }) => {
|
||||
const full = readBrandSkill()
|
||||
|
||||
if (!section || section === 'all') {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: full }],
|
||||
}
|
||||
}
|
||||
|
||||
// Section anchors in BRAND.md (markdown H2 headings)
|
||||
const anchors: Record<string, RegExp> = {
|
||||
positioning: /## 2\. Core Positioning[\s\S]*?(?=\n## |\n---)/,
|
||||
axes: /## 3\. The Three Brand Axes[\s\S]*?(?=\n## |\n---)/,
|
||||
tone: /## 4\. Tone of Voice[\s\S]*?(?=\n## |\n---)/,
|
||||
'writing-rules': /## 5\. Writing Rules[\s\S]*?(?=\n## |\n---)/,
|
||||
'reasoning-patterns': /## 6\. Patterns for Reasoning[\s\S]*?(?=\n## |\n---)/,
|
||||
cta: /## 7\. CTA Guidance[\s\S]*?(?=\n## |\n---)/,
|
||||
logo: /## 10\. Logo Usage[\s\S]*?(?=\n## |\n---)/,
|
||||
'self-check': /## 11\. Self-check[\s\S]*?(?=\n## |\n---)/,
|
||||
}
|
||||
|
||||
const re = anchors[section]
|
||||
const match = re ? full.match(re) : null
|
||||
|
||||
if (!match) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Section "${section}" not found. Returning full brand skill instead.\n\n${full}`,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: match[0] }],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.tool(
|
||||
'validate_copy',
|
||||
'Checks a piece of user-facing copy against Greyhaven brand rules. Flags hype words, sales language, vague superlatives, and other brand violations. Use on marketing copy, CTAs, headlines, product descriptions before shipping.',
|
||||
{ text: z.string().describe('The copy to validate') },
|
||||
async ({ text }) => {
|
||||
const lower = text.toLowerCase()
|
||||
|
||||
const bannedWords = [
|
||||
'unleash', 'transform', 'revolutionary', 'revolutionize',
|
||||
'seamless', 'seamlessly', 'game-changing', 'cutting-edge',
|
||||
'next-gen', 'next-generation', 'leverage', 'synergy', 'unlock',
|
||||
'supercharge', 'empower', 'empowered', 'unprecedented',
|
||||
'best-in-class', 'industry-leading', 'world-class',
|
||||
'lightning-fast', 'blazing fast',
|
||||
]
|
||||
|
||||
const vagueSuperlatives = [
|
||||
'amazing', 'incredible', 'awesome', 'stunning', 'beautiful',
|
||||
'powerful', 'robust', 'cutting edge', 'state-of-the-art',
|
||||
]
|
||||
|
||||
const urgencyPhrases = [
|
||||
'limited time', 'act now', "don't miss out", 'hurry',
|
||||
'last chance', 'today only',
|
||||
]
|
||||
|
||||
const findings: string[] = []
|
||||
|
||||
for (const w of bannedWords) {
|
||||
if (lower.includes(w)) {
|
||||
findings.push(`⚠ Banned hype/sales word: "${w}"`)
|
||||
}
|
||||
}
|
||||
for (const w of vagueSuperlatives) {
|
||||
if (lower.includes(w)) {
|
||||
findings.push(`⚠ Vague superlative: "${w}" — replace with specifics`)
|
||||
}
|
||||
}
|
||||
for (const p of urgencyPhrases) {
|
||||
if (lower.includes(p)) {
|
||||
findings.push(`⚠ Urgency framing: "${p}" — Greyhaven does not use urgency`)
|
||||
}
|
||||
}
|
||||
|
||||
// Exclamation marks
|
||||
const exclamations = (text.match(/!/g) || []).length
|
||||
if (exclamations > 0) {
|
||||
findings.push(`⚠ Found ${exclamations} exclamation mark(s) — Greyhaven copy does not use them`)
|
||||
}
|
||||
|
||||
if (findings.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: 'No obvious brand violations found. Still run the self-check list from get_brand_rules({section: "self-check"}) before shipping.',
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Found ${findings.length} potential brand violation(s):\n\n${findings.join('\n')}\n\nFor detailed guidance, call get_brand_rules() or get_brand_rules({section: "tone"}).`,
|
||||
}],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resources
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
server.resource('brand://guidelines', 'brand://guidelines', async (uri) => {
|
||||
return {
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
mimeType: 'text/markdown',
|
||||
text: readBrandSkill(),
|
||||
}],
|
||||
}
|
||||
})
|
||||
|
||||
server.resource('tokens://all', 'tokens://all', async (uri) => {
|
||||
const tokens = getTokens(ROOT)
|
||||
return {
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(tokens, null, 2),
|
||||
}],
|
||||
}
|
||||
})
|
||||
|
||||
for (const component of COMPONENT_CATALOG) {
|
||||
const uri = `component://${component.name.toLowerCase()}`
|
||||
server.resource(uri, uri, async (resourceUri) => {
|
||||
let source = ''
|
||||
try {
|
||||
source = fs.readFileSync(path.join(ROOT, component.file), 'utf-8')
|
||||
} catch {
|
||||
source = '(source not readable)'
|
||||
}
|
||||
|
||||
return {
|
||||
contents: [{
|
||||
uri: resourceUri.href,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({ ...component, source }, null, 2),
|
||||
}],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Start
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport()
|
||||
await server.connect(transport)
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
14
mcp/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "..",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["server.ts", "../lib/catalog.ts"]
|
||||
}
|
||||
@@ -6,6 +6,9 @@ const nextConfig = {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
turbopack: {
|
||||
root: '.',
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
||||
29
package.json
@@ -3,10 +3,16 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"tokens:build": "npx style-dictionary build --config style-dictionary.config.mjs",
|
||||
"skill:build": "npx tsx scripts/generate-skill.ts",
|
||||
"build": "pnpm tokens:build && pnpm skill:build && next build",
|
||||
"dev": "next dev",
|
||||
"lint": "eslint .",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"mcp:build": "npx tsc -p mcp/tsconfig.json",
|
||||
"mcp:start": "npx tsx mcp/server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
@@ -61,13 +67,28 @@
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^5.1.2",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@storybook/addon-a11y": "^10.3.5",
|
||||
"@storybook/addon-docs": "^10.3.5",
|
||||
"@storybook/addon-onboarding": "^10.3.5",
|
||||
"@storybook/addon-vitest": "^10.3.5",
|
||||
"@storybook/nextjs-vite": "^10.3.5",
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitest/browser-playwright": "^4.1.4",
|
||||
"@vitest/coverage-v8": "^4.1.4",
|
||||
"playwright": "^1.59.1",
|
||||
"postcss": "^8.5",
|
||||
"storybook": "^10.3.5",
|
||||
"style-dictionary": "^4.4.0",
|
||||
"tailwindcss": "^4.1.9",
|
||||
"tsx": "^4.21.0",
|
||||
"tw-animate-css": "1.3.3",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4403
pnpm-lock.yaml
generated
BIN
public/fonts/Aspekta-100.woff2
Normal file
BIN
public/fonts/Aspekta-1000.woff2
Normal file
BIN
public/fonts/Aspekta-150.woff2
Normal file
BIN
public/fonts/Aspekta-200.woff2
Normal file
BIN
public/fonts/Aspekta-250.woff2
Normal file
BIN
public/fonts/Aspekta-300.woff2
Normal file
BIN
public/fonts/Aspekta-350.woff2
Normal file
BIN
public/fonts/Aspekta-400.woff2
Normal file
BIN
public/fonts/Aspekta-450.woff2
Normal file
BIN
public/fonts/Aspekta-50.woff2
Normal file
BIN
public/fonts/Aspekta-500.woff2
Normal file
BIN
public/fonts/Aspekta-550.woff2
Normal file
BIN
public/fonts/Aspekta-600.woff2
Normal file
BIN
public/fonts/Aspekta-650.woff2
Normal file
BIN
public/fonts/Aspekta-700.woff2
Normal file
BIN
public/fonts/Aspekta-750.woff2
Normal file
BIN
public/fonts/Aspekta-800.woff2
Normal file
BIN
public/fonts/Aspekta-850.woff2
Normal file
BIN
public/fonts/Aspekta-900.woff2
Normal file
BIN
public/fonts/Aspekta-950.woff2
Normal file
161
public/fonts/font-face.css
Normal file
@@ -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');
|
||||
}
|
||||
1
public/gh - logo - offblack.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660.98 230.26"><g id="c"><path d="M239.51,143.89l-.85-8.54c-2.94,4.93-9.87,9.97-20.6,9.97-17.56,0-33.13-12.91-33.13-35.12s16.42-35.02,33.88-35.02c16.33,0,26.77,9.4,30.37,20.5l-10.73,4.08c-2.28-7.59-8.92-14.05-19.65-14.05s-22.3,7.78-22.3,24.49,10.63,24.68,22.21,24.68c13.57,0,18.7-9.3,19.27-15.09h-22.21v-9.97h32.94v34.07h-9.21Z" fill="#171715"/><path d="M286.39,108.58c-1.23-.19-2.47-.28-3.61-.28-8.54,0-12.43,4.93-12.43,13.57v22.02h-11.01v-46.22h10.73v7.4c2.18-5.03,7.31-7.97,13.38-7.97,1.33,0,2.47.19,2.94.28v11.2Z" fill="#171715"/><path d="M333.94,130.7c-2.47,8.07-9.78,14.62-20.88,14.62-12.53,0-23.63-9.11-23.63-24.77,0-14.62,10.82-24.3,22.49-24.3,14.24,0,22.59,9.4,22.59,24.01,0,1.8-.19,3.32-.29,3.51h-33.79c.28,7.02,5.79,12.05,12.62,12.05s10.06-3.51,11.58-8.07l9.3,2.94ZM323.41,115.7c-.19-5.41-3.8-10.25-11.39-10.25-6.93,0-10.91,5.31-11.29,10.25h22.68Z" fill="#171715"/><path d="M343.57,162.59l11.11-24.2-19.74-40.72h12.43l13.29,29.23,12.43-29.23h11.67l-29.42,64.92h-11.77Z" fill="#171715"/><path d="M400.95,143.89h-11.01v-68.72h11.01v26.96c3.13-4.08,8.35-5.79,13.19-5.79,11.39,0,16.89,8.16,16.89,18.32v29.23h-11.01v-27.34c0-5.7-2.56-10.25-9.49-10.25-6.07,0-9.4,4.55-9.59,10.44v27.15Z" fill="#171715"/><path d="M452.54,117.41l11.86-1.8c2.66-.38,3.42-1.71,3.42-3.32,0-3.89-2.66-7.02-8.73-7.02s-9.02,3.7-9.49,8.35l-10.06-2.28c.85-7.97,8.07-15.09,19.46-15.09,14.24,0,19.65,8.07,19.65,17.27v22.97c0,4.18.48,6.93.57,7.4h-10.25c-.09-.29-.47-2.18-.47-5.89-2.18,3.51-6.74,7.31-14.24,7.31-9.68,0-15.66-6.64-15.66-13.95,0-8.26,6.07-12.81,13.95-13.95ZM467.82,124.72v-2.09l-12.05,1.8c-3.42.57-6.17,2.47-6.17,6.27,0,3.13,2.37,5.98,6.74,5.98,6.17,0,11.48-2.94,11.48-11.96Z" fill="#171715"/><path d="M509.42,143.89h-11.01l-18.79-46.22h12.15l12.24,33.03,11.96-33.03h11.58l-18.13,46.22Z" fill="#171715"/><path d="M572.91,130.7c-2.47,8.07-9.78,14.62-20.88,14.62-12.53,0-23.63-9.11-23.63-24.77,0-14.62,10.82-24.3,22.49-24.3,14.24,0,22.59,9.4,22.59,24.01,0,1.8-.19,3.32-.29,3.51h-33.79c.28,7.02,5.79,12.05,12.62,12.05s10.06-3.51,11.58-8.07l9.3,2.94ZM562.38,115.7c-.19-5.41-3.8-10.25-11.39-10.25-6.93,0-10.91,5.31-11.29,10.25h22.68Z" fill="#171715"/><path d="M592.54,143.89h-11.01v-46.22h10.73v6.17c3.04-5.31,8.54-7.5,13.67-7.5,11.29,0,16.7,8.16,16.7,18.32v29.23h-11.01v-27.34c0-5.7-2.56-10.25-9.49-10.25-6.26,0-9.59,4.84-9.59,10.91v26.67Z" fill="#171715"/><path d="M149.44,75.45l-18.67-10.96h-1.01l-16.68,9.79v-20.19s-18.67-10.96-18.67-10.96h-1.01l-18.67,10.96v20.19s-16.68-9.79-16.68-9.79h-1.01l-18.67,10.96v.72s0,78.5,0,78.5l18.67,11.12h1.02l16.67-9.92v20.16s18.67,11.12,18.67,11.12h1.02l18.67-11.12v-20.16s16.67,9.92,16.67,9.92h1.02l18.67-11.12v-79.21ZM57.54,66.51l16.7,9.81-16.7,9.94-16.7-9.94,16.7-9.81ZM40.36,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM56.54,144.59v18.57l-15.7-9.35,15.7-9.22ZM58.54,163.16v-18.57l15.7,9.22-15.7,9.35ZM74.72,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41ZM111.08,76.17v19.46l-16.18-9.5v-19.49l16.18-9.63v19.16ZM93.9,107.61l-16.7-9.94,16.7-9.81,16.7,9.81-16.7,9.94ZM94.9,64.31v-18.57l15.7,9.22-15.7,9.35ZM92.9,45.74v18.57l-15.7-9.35,15.7-9.22ZM76.72,57l16.18,9.63v19.49l-16.18,9.5v-38.63ZM76.72,99.71l16.18,9.63v54.28l-16.18,9.5v-73.41ZM92.9,165.94v18.57l-15.7-9.35,15.7-9.22ZM94.9,184.51v-18.57l15.7,9.22-15.7,9.35ZM111.08,173.12l-16.18-9.5v-54.28l16.18-9.63v73.41ZM131.26,67.09l15.7,9.22-15.7,9.35v-18.57ZM129.26,67.09v18.57l-15.7-9.35,15.7-9.22ZM113.08,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM130.26,163.75l-16.7-9.95,16.7-9.81,16.7,9.81-16.7,9.95ZM147.44,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41Z" fill="#171715"/><rect x="0" width="660.98" height="230.26" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
public/gh - logo - positive - full black.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660.98 230.26"><g id="c"><path d="M239.51,143.89l-.85-8.54c-2.94,4.93-9.87,9.97-20.6,9.97-17.56,0-33.13-12.91-33.13-35.12s16.42-35.02,33.88-35.02c16.33,0,26.77,9.4,30.37,20.5l-10.73,4.08c-2.28-7.59-8.92-14.05-19.65-14.05s-22.3,7.78-22.3,24.49,10.63,24.68,22.21,24.68c13.57,0,18.7-9.3,19.27-15.09h-22.21v-9.97h32.94v34.07h-9.21Z"/><path d="M286.39,108.58c-1.23-.19-2.47-.28-3.61-.28-8.54,0-12.43,4.93-12.43,13.57v22.02h-11.01v-46.22h10.73v7.4c2.18-5.03,7.31-7.97,13.38-7.97,1.33,0,2.47.19,2.94.28v11.2Z"/><path d="M333.94,130.7c-2.47,8.07-9.78,14.62-20.88,14.62-12.53,0-23.63-9.11-23.63-24.77,0-14.62,10.82-24.3,22.49-24.3,14.24,0,22.59,9.4,22.59,24.01,0,1.8-.19,3.32-.29,3.51h-33.79c.28,7.02,5.79,12.05,12.62,12.05s10.06-3.51,11.58-8.07l9.3,2.94ZM323.41,115.7c-.19-5.41-3.8-10.25-11.39-10.25-6.93,0-10.91,5.31-11.29,10.25h22.68Z"/><path d="M343.57,162.59l11.11-24.2-19.74-40.72h12.43l13.29,29.23,12.43-29.23h11.67l-29.42,64.92h-11.77Z"/><path d="M400.95,143.89h-11.01v-68.72h11.01v26.96c3.13-4.08,8.35-5.79,13.19-5.79,11.39,0,16.89,8.16,16.89,18.32v29.23h-11.01v-27.34c0-5.7-2.56-10.25-9.49-10.25-6.07,0-9.4,4.55-9.59,10.44v27.15Z"/><path d="M452.54,117.41l11.86-1.8c2.66-.38,3.42-1.71,3.42-3.32,0-3.89-2.66-7.02-8.73-7.02s-9.02,3.7-9.49,8.35l-10.06-2.28c.85-7.97,8.07-15.09,19.46-15.09,14.24,0,19.65,8.07,19.65,17.27v22.97c0,4.18.48,6.93.57,7.4h-10.25c-.09-.29-.47-2.18-.47-5.89-2.18,3.51-6.74,7.31-14.24,7.31-9.68,0-15.66-6.64-15.66-13.95,0-8.26,6.07-12.81,13.95-13.95ZM467.82,124.72v-2.09l-12.05,1.8c-3.42.57-6.17,2.47-6.17,6.27,0,3.13,2.37,5.98,6.74,5.98,6.17,0,11.48-2.94,11.48-11.96Z"/><path d="M509.42,143.89h-11.01l-18.79-46.22h12.15l12.24,33.03,11.96-33.03h11.58l-18.13,46.22Z"/><path d="M572.91,130.7c-2.47,8.07-9.78,14.62-20.88,14.62-12.53,0-23.63-9.11-23.63-24.77,0-14.62,10.82-24.3,22.49-24.3,14.24,0,22.59,9.4,22.59,24.01,0,1.8-.19,3.32-.29,3.51h-33.79c.28,7.02,5.79,12.05,12.62,12.05s10.06-3.51,11.58-8.07l9.3,2.94ZM562.38,115.7c-.19-5.41-3.8-10.25-11.39-10.25-6.93,0-10.91,5.31-11.29,10.25h22.68Z"/><path d="M592.54,143.89h-11.01v-46.22h10.73v6.17c3.04-5.31,8.54-7.5,13.67-7.5,11.29,0,16.7,8.16,16.7,18.32v29.23h-11.01v-27.34c0-5.7-2.56-10.25-9.49-10.25-6.26,0-9.59,4.84-9.59,10.91v26.67Z"/><path d="M149.44,75.45l-18.67-10.96h-1.01l-16.68,9.79v-20.19s-18.67-10.96-18.67-10.96h-1.01l-18.67,10.96v20.19s-16.68-9.79-16.68-9.79h-1.01l-18.67,10.96v.72s0,78.5,0,78.5l18.67,11.12h1.02l16.67-9.92v20.16s18.67,11.12,18.67,11.12h1.02l18.67-11.12v-20.16s16.67,9.92,16.67,9.92h1.02l18.67-11.12v-79.21ZM57.54,66.51l16.7,9.81-16.7,9.94-16.7-9.94,16.7-9.81ZM40.36,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM56.54,144.59v18.57l-15.7-9.35,15.7-9.22ZM58.54,163.16v-18.57l15.7,9.22-15.7,9.35ZM74.72,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41ZM111.08,76.17v19.46l-16.18-9.5v-19.49l16.18-9.63v19.16ZM93.9,107.61l-16.7-9.94,16.7-9.81,16.7,9.81-16.7,9.94ZM94.9,64.31v-18.57l15.7,9.22-15.7,9.35ZM92.9,45.74v18.57l-15.7-9.35,15.7-9.22ZM76.72,57l16.18,9.63v19.49l-16.18,9.5v-38.63ZM76.72,99.71l16.18,9.63v54.28l-16.18,9.5v-73.41ZM92.9,165.94v18.57l-15.7-9.35,15.7-9.22ZM94.9,184.51v-18.57l15.7,9.22-15.7,9.35ZM111.08,173.12l-16.18-9.5v-54.28l16.18-9.63v73.41ZM131.26,67.09l15.7,9.22-15.7,9.35v-18.57ZM129.26,67.09v18.57l-15.7-9.35,15.7-9.22ZM113.08,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM130.26,163.75l-16.7-9.95,16.7-9.81,16.7,9.81-16.7,9.95ZM147.44,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41Z"/><rect width="660.98" height="230.26" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
1
public/gh - logo - white.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660.98 230.26"><g id="c"><path d="M239.51,143.89l-.85-8.54c-2.94,4.93-9.87,9.97-20.6,9.97-17.56,0-33.13-12.91-33.13-35.12s16.42-35.02,33.88-35.02c16.33,0,26.77,9.4,30.37,20.5l-10.73,4.08c-2.28-7.59-8.92-14.05-19.65-14.05s-22.3,7.78-22.3,24.49,10.63,24.68,22.21,24.68c13.57,0,18.7-9.3,19.27-15.09h-22.21v-9.97h32.94v34.07h-9.21Z" fill="#fff"/><path d="M286.39,108.58c-1.23-.19-2.47-.28-3.61-.28-8.54,0-12.43,4.93-12.43,13.57v22.02h-11.01v-46.22h10.73v7.4c2.18-5.03,7.31-7.97,13.38-7.97,1.33,0,2.47.19,2.94.28v11.2Z" fill="#fff"/><path d="M333.94,130.7c-2.47,8.07-9.78,14.62-20.88,14.62-12.53,0-23.63-9.11-23.63-24.77,0-14.62,10.82-24.3,22.49-24.3,14.24,0,22.59,9.4,22.59,24.01,0,1.8-.19,3.32-.29,3.51h-33.79c.28,7.02,5.79,12.05,12.62,12.05s10.06-3.51,11.58-8.07l9.3,2.94ZM323.41,115.7c-.19-5.41-3.8-10.25-11.39-10.25-6.93,0-10.91,5.31-11.29,10.25h22.68Z" fill="#fff"/><path d="M343.57,162.59l11.11-24.2-19.74-40.72h12.43l13.29,29.23,12.43-29.23h11.67l-29.42,64.92h-11.77Z" fill="#fff"/><path d="M400.95,143.89h-11.01v-68.72h11.01v26.96c3.13-4.08,8.35-5.79,13.19-5.79,11.39,0,16.89,8.16,16.89,18.32v29.23h-11.01v-27.34c0-5.7-2.56-10.25-9.49-10.25-6.07,0-9.4,4.55-9.59,10.44v27.15Z" fill="#fff"/><path d="M452.54,117.41l11.86-1.8c2.66-.38,3.42-1.71,3.42-3.32,0-3.89-2.66-7.02-8.73-7.02s-9.02,3.7-9.49,8.35l-10.06-2.28c.85-7.97,8.07-15.09,19.46-15.09,14.24,0,19.65,8.07,19.65,17.27v22.97c0,4.18.48,6.93.57,7.4h-10.25c-.09-.29-.47-2.18-.47-5.89-2.18,3.51-6.74,7.31-14.24,7.31-9.68,0-15.66-6.64-15.66-13.95,0-8.26,6.07-12.81,13.95-13.95ZM467.82,124.72v-2.09l-12.05,1.8c-3.42.57-6.17,2.47-6.17,6.27,0,3.13,2.37,5.98,6.74,5.98,6.17,0,11.48-2.94,11.48-11.96Z" fill="#fff"/><path d="M509.42,143.89h-11.01l-18.79-46.22h12.15l12.24,33.03,11.96-33.03h11.58l-18.13,46.22Z" fill="#fff"/><path d="M572.91,130.7c-2.47,8.07-9.78,14.62-20.88,14.62-12.53,0-23.63-9.11-23.63-24.77,0-14.62,10.82-24.3,22.49-24.3,14.24,0,22.59,9.4,22.59,24.01,0,1.8-.19,3.32-.29,3.51h-33.79c.28,7.02,5.79,12.05,12.62,12.05s10.06-3.51,11.58-8.07l9.3,2.94ZM562.38,115.7c-.19-5.41-3.8-10.25-11.39-10.25-6.93,0-10.91,5.31-11.29,10.25h22.68Z" fill="#fff"/><path d="M592.54,143.89h-11.01v-46.22h10.73v6.17c3.04-5.31,8.54-7.5,13.67-7.5,11.29,0,16.7,8.16,16.7,18.32v29.23h-11.01v-27.34c0-5.7-2.56-10.25-9.49-10.25-6.26,0-9.59,4.84-9.59,10.91v26.67Z" fill="#fff"/><path d="M149.44,75.45l-18.67-10.96h-1.01l-16.68,9.79v-20.19s-18.67-10.96-18.67-10.96h-1.01l-18.67,10.96v20.19s-16.68-9.79-16.68-9.79h-1.01l-18.67,10.96v.72s0,78.5,0,78.5l18.67,11.12h1.02l16.67-9.92v20.16s18.67,11.12,18.67,11.12h1.02l18.67-11.12v-20.16s16.67,9.92,16.67,9.92h1.02l18.67-11.12v-79.21ZM57.54,66.51l16.7,9.81-16.7,9.94-16.7-9.94,16.7-9.81ZM40.36,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM56.54,144.59v18.57l-15.7-9.35,15.7-9.22ZM58.54,163.16v-18.57l15.7,9.22-15.7,9.35ZM74.72,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41ZM111.08,76.17v19.46l-16.18-9.5v-19.49l16.18-9.63v19.16ZM93.9,107.61l-16.7-9.94,16.7-9.81,16.7,9.81-16.7,9.94ZM94.9,64.31v-18.57l15.7,9.22-15.7,9.35ZM92.9,45.74v18.57l-15.7-9.35,15.7-9.22ZM76.72,57l16.18,9.63v19.49l-16.18,9.5v-38.63ZM76.72,99.71l16.18,9.63v54.28l-16.18,9.5v-73.41ZM92.9,165.94v18.57l-15.7-9.35,15.7-9.22ZM94.9,184.51v-18.57l15.7,9.22-15.7,9.35ZM111.08,173.12l-16.18-9.5v-54.28l16.18-9.63v73.41ZM131.26,67.09l15.7,9.22-15.7,9.35v-18.57ZM129.26,67.09v18.57l-15.7-9.35,15.7-9.22ZM113.08,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM130.26,163.75l-16.7-9.95,16.7-9.81,16.7,9.81-16.7,9.95ZM147.44,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41Z" fill="#fff"/><rect width="660.98" height="230.26" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
public/gh - symbol - full black.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 151.08 184"><g id="c"><path d="M131.08,52.32l-18.67-10.96h-1.01l-16.68,9.79v-20.19s-18.67-10.96-18.67-10.96h-1.01l-18.67,10.96v20.19s-16.68-9.79-16.68-9.79h-1.01l-18.67,10.96v.72s0,78.5,0,78.5l18.67,11.12h1.02l16.67-9.92v20.16s18.67,11.12,18.67,11.12h1.02l18.67-11.12v-20.16s16.67,9.92,16.67,9.92h1.02l18.67-11.12V52.32ZM39.18,43.38l16.7,9.81-16.7,9.94-16.7-9.94,16.7-9.81ZM22,55.23l16.18,9.63v54.28l-16.18,9.5V55.23ZM38.18,121.46v18.57l-15.7-9.35,15.7-9.22ZM40.18,140.03v-18.57l15.7,9.22-15.7,9.35ZM56.36,128.64l-16.18-9.5v-54.28l16.18-9.63v73.41ZM92.72,53.04v19.46l-16.18-9.5v-19.49l16.18-9.63v19.16ZM75.54,84.48l-16.7-9.94,16.7-9.81,16.7,9.81-16.7,9.94ZM76.54,41.18v-18.57l15.7,9.22-15.7,9.35ZM74.54,22.61v18.57l-15.7-9.35,15.7-9.22ZM58.36,33.87l16.18,9.63v19.49l-16.18,9.5v-38.63ZM58.36,76.58l16.18,9.63v54.28l-16.18,9.5v-73.41ZM74.54,142.81v18.57l-15.7-9.35,15.7-9.22ZM76.54,161.38v-18.57l15.7,9.22-15.7,9.35ZM92.72,149.99l-16.18-9.5v-54.28l16.18-9.63v73.41ZM112.9,43.97l15.7,9.22-15.7,9.35v-18.57ZM110.9,43.97v18.57l-15.7-9.35,15.7-9.22ZM94.72,55.23l16.18,9.63v54.28l-16.18,9.5V55.23ZM111.9,140.63l-16.7-9.95,16.7-9.81,16.7,9.81-16.7,9.95ZM129.08,128.64l-16.18-9.5v-54.28l16.18-9.63v73.41Z"/><rect width="151.08" height="184" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/gh - symbol - full white.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 151.08 184"><g id="c"><path d="M131.08,52.32l-18.67-10.96h-1.01l-16.68,9.79v-20.19s-18.67-10.96-18.67-10.96h-1.01l-18.67,10.96v20.19s-16.68-9.79-16.68-9.79h-1.01l-18.67,10.96v.72s0,78.5,0,78.5l18.67,11.12h1.02l16.67-9.92v20.16s18.67,11.12,18.67,11.12h1.02l18.67-11.12v-20.16s16.67,9.92,16.67,9.92h1.02l18.67-11.12V52.32ZM39.18,43.38l16.7,9.81-16.7,9.94-16.7-9.94,16.7-9.81ZM22,55.23l16.18,9.63v54.28l-16.18,9.5V55.23ZM38.18,121.46v18.57l-15.7-9.35,15.7-9.22ZM40.18,140.03v-18.57l15.7,9.22-15.7,9.35ZM56.36,128.64l-16.18-9.5v-54.28l16.18-9.63v73.41ZM92.72,53.04v19.46l-16.18-9.5v-19.49l16.18-9.63v19.16ZM75.54,84.48l-16.7-9.94,16.7-9.81,16.7,9.81-16.7,9.94ZM76.54,41.18v-18.57l15.7,9.22-15.7,9.35ZM74.54,22.61v18.57l-15.7-9.35,15.7-9.22ZM58.36,33.87l16.18,9.63v19.49l-16.18,9.5v-38.63ZM58.36,76.58l16.18,9.63v54.28l-16.18,9.5v-73.41ZM74.54,142.81v18.57l-15.7-9.35,15.7-9.22ZM76.54,161.38v-18.57l15.7,9.22-15.7,9.35ZM92.72,149.99l-16.18-9.5v-54.28l16.18-9.63v73.41ZM112.9,43.97l15.7,9.22-15.7,9.35v-18.57ZM110.9,43.97v18.57l-15.7-9.35,15.7-9.22ZM94.72,55.23l16.18,9.63v54.28l-16.18,9.5V55.23ZM111.9,140.63l-16.7-9.95,16.7-9.81,16.7,9.81-16.7,9.95ZM129.08,128.64l-16.18-9.5v-54.28l16.18-9.63v73.41Z" fill="#fff"/><rect width="151.08" height="184" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/greyproxy - positive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660.98 230.26"><g id="c"><path d="M153.83,75.45l-18.67-10.96h-1.01l-16.68,9.79v-20.19s-18.67-10.96-18.67-10.96h-1.01l-18.67,10.96v20.19s-16.68-9.79-16.68-9.79h-1.01l-18.67,10.96v.72s0,78.5,0,78.5l18.67,11.12h1.02l16.67-9.92v20.16s18.67,11.12,18.67,11.12h1.02l18.67-11.12v-20.16s16.67,9.92,16.67,9.92h1.02l18.67-11.12v-79.21ZM61.93,66.51l16.7,9.81-16.7,9.94-16.7-9.94,16.7-9.81ZM44.75,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM60.93,144.59v18.57l-15.7-9.35,15.7-9.22ZM62.93,163.16v-18.57l15.7,9.22-15.7,9.35ZM79.11,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41ZM115.47,76.17v19.46l-16.18-9.5v-19.49l16.18-9.63v19.16ZM98.29,107.61l-16.7-9.94,16.7-9.81,16.7,9.81-16.7,9.94ZM99.29,64.31v-18.57l15.7,9.22-15.7,9.35ZM97.29,45.74v18.57l-15.7-9.35,15.7-9.22ZM81.11,57l16.18,9.63v19.49l-16.18,9.5v-38.63ZM81.11,99.71l16.18,9.63v54.28l-16.18,9.5v-73.41ZM97.29,165.94v18.57l-15.7-9.35,15.7-9.22ZM99.29,184.51v-18.57l15.7,9.22-15.7,9.35ZM115.47,173.12l-16.18-9.5v-54.28l16.18-9.63v73.41ZM135.66,67.09l15.7,9.22-15.7,9.35v-18.57ZM133.66,67.09v18.57l-15.7-9.35,15.7-9.22ZM117.47,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM134.66,163.75l-16.7-9.95,16.7-9.81,16.7,9.81-16.7,9.95ZM151.84,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41Z"/><path d="M245.09,143.61l-.85-8.52c-2.93,4.92-9.84,9.93-20.53,9.93-17.5,0-33.02-12.87-33.02-35.01s16.37-34.92,33.78-34.92c16.27,0,26.68,9.37,30.28,20.44l-10.69,4.07c-2.27-7.57-8.89-14-19.59-14s-22.24,7.76-22.24,24.41,10.6,24.6,22.14,24.6c13.53,0,18.64-9.27,19.21-15.04h-22.14v-9.94h32.83v33.97h-9.18Z"/><path d="M291.54,108.41c-1.23-.19-2.46-.28-3.6-.28-8.52,0-12.39,4.92-12.39,13.53v21.95h-10.98v-46.08h10.69v7.38c2.18-5.01,7.29-7.95,13.34-7.95,1.33,0,2.46.19,2.93.28v11.17Z"/><path d="M338.36,130.46c-2.46,8.04-9.75,14.57-20.82,14.57-12.49,0-23.56-9.08-23.56-24.7,0-14.57,10.79-24.22,22.42-24.22,14.19,0,22.52,9.37,22.52,23.94,0,1.8-.19,3.31-.28,3.5h-33.68c.28,7,5.77,12.02,12.58,12.02s10.03-3.5,11.54-8.04l9.27,2.93ZM327.86,115.51c-.19-5.39-3.79-10.22-11.35-10.22-6.91,0-10.88,5.3-11.26,10.22h22.61Z"/><path d="M347.66,162.25l11.07-24.13-19.68-40.59h12.39l13.25,29.14,12.4-29.14h11.64l-29.33,64.72h-11.73Z"/><path d="M393.56,161.59v-64.06h10.6v6.25c2.27-3.97,7.57-7.29,14.67-7.29,13.62,0,21.29,10.41,21.29,24.03s-8.42,24.22-21.67,24.22c-6.62,0-11.64-2.84-14-6.34v23.18h-10.88ZM416.74,106.23c-7.19,0-12.39,5.68-12.39,14.29s5.2,14.48,12.39,14.48,12.4-5.68,12.4-14.48-5.01-14.29-12.4-14.29Z"/><path d="M472.94,108.41c-1.23-.19-2.46-.28-3.6-.28-8.52,0-12.39,4.92-12.39,13.53v21.95h-10.98v-46.08h10.69v7.38c2.18-5.01,7.29-7.95,13.34-7.95,1.33,0,2.46.19,2.93.28v11.17Z"/><path d="M521.49,120.52c0,14.19-10.12,24.51-23.94,24.51s-23.84-10.31-23.84-24.51,10.12-24.41,23.84-24.41,23.94,10.31,23.94,24.41ZM510.42,120.52c0-9.56-6.06-14.48-12.87-14.48s-12.87,4.92-12.87,14.48,6.15,14.67,12.87,14.67,12.87-5.01,12.87-14.67Z"/><path d="M538.37,120.33l-16.46-22.8h13.06c.85,1.42,9.27,13.44,10.12,14.76l10.03-14.76h12.49l-16.18,22.61,16.75,23.47h-12.87l-10.69-15.42c-.95,1.42-9.56,14-10.41,15.42h-12.58l16.75-23.28Z"/><path d="M577.17,162.25l11.07-24.13-19.68-40.59h12.4l13.25,29.14,12.39-29.14h11.64l-29.33,64.72h-11.73Z"/><rect width="660.98" height="230.26" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
1
public/greywall - positive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 630.98 230.26"><g id="c"><path d="M173.11,75.45l-18.67-10.96h-1.01l-16.68,9.79v-20.19s-18.67-10.96-18.67-10.96h-1.01l-18.67,10.96v20.19s-16.68-9.79-16.68-9.79h-1.01l-18.67,10.96v.72s0,78.5,0,78.5l18.67,11.12h1.02l16.67-9.92v20.16s18.67,11.12,18.67,11.12h1.02l18.67-11.12v-20.16s16.67,9.92,16.67,9.92h1.02l18.67-11.12v-79.21ZM81.21,66.51l16.7,9.81-16.7,9.94-16.7-9.94,16.7-9.81ZM64.03,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM80.21,144.59v18.57l-15.7-9.35,15.7-9.22ZM82.21,163.16v-18.57l15.7,9.22-15.7,9.35ZM98.39,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41ZM134.76,76.17v19.46l-16.18-9.5v-19.49l16.18-9.63v19.16ZM117.58,107.61l-16.7-9.94,16.7-9.81,16.7,9.81-16.7,9.94ZM118.58,64.31v-18.57l15.7,9.22-15.7,9.35ZM116.58,45.74v18.57l-15.7-9.35,15.7-9.22ZM100.4,57l16.18,9.63v19.49l-16.18,9.5v-38.63ZM100.4,99.71l16.18,9.63v54.28l-16.18,9.5v-73.41ZM116.58,165.94v18.57l-15.7-9.35,15.7-9.22ZM118.58,184.51v-18.57l15.7,9.22-15.7,9.35ZM134.76,173.12l-16.18-9.5v-54.28l16.18-9.63v73.41ZM154.94,67.09l15.7,9.22-15.7,9.35v-18.57ZM152.94,67.09v18.57l-15.7-9.35,15.7-9.22ZM136.76,78.36l16.18,9.63v54.28l-16.18,9.5v-73.41ZM153.94,163.75l-16.7-9.95,16.7-9.81,16.7,9.81-16.7,9.95ZM171.12,151.77l-16.18-9.5v-54.28l16.18-9.63v73.41Z"/><path d="M264.37,143.61l-.85-8.52c-2.93,4.92-9.84,9.93-20.53,9.93-17.51,0-33.02-12.87-33.02-35.01s16.37-34.92,33.78-34.92c16.28,0,26.68,9.37,30.28,20.44l-10.69,4.07c-2.27-7.57-8.89-14-19.59-14s-22.24,7.76-22.24,24.41,10.6,24.6,22.14,24.6c13.53,0,18.64-9.27,19.21-15.04h-22.14v-9.94h32.83v33.97h-9.18Z" fill="#231f20"/><path d="M310.82,108.41c-1.23-.19-2.46-.28-3.6-.28-8.52,0-12.4,4.92-12.4,13.53v21.95h-10.98v-46.08h10.69v7.38c2.18-5.01,7.29-7.95,13.34-7.95,1.32,0,2.46.19,2.93.28v11.17Z" fill="#231f20"/><path d="M357.92,130.46c-2.46,8.04-9.75,14.57-20.82,14.57-12.49,0-23.56-9.08-23.56-24.7,0-14.57,10.79-24.22,22.43-24.22,14.19,0,22.52,9.37,22.52,23.94,0,1.8-.19,3.31-.28,3.5h-33.68c.28,7,5.77,12.02,12.58,12.02s10.03-3.5,11.54-8.04l9.27,2.93ZM347.42,115.51c-.19-5.39-3.79-10.22-11.36-10.22-6.91,0-10.88,5.3-11.26,10.22h22.62Z" fill="#231f20"/><path d="M367.23,162.25l11.07-24.13-19.68-40.59h12.4l13.25,29.14,12.39-29.14h11.64l-29.33,64.72h-11.73Z" fill="#231f20"/><path d="M454.23,97.53l10.88,31.89,9.18-31.89h11.17l-14.76,46.08h-10.98l-11.45-33.12-11.17,33.12h-11.26l-14.95-46.08h11.73l9.37,31.89,10.88-31.89h11.35Z" fill="#231f20"/><path d="M501.01,117.21l11.83-1.8c2.65-.38,3.41-1.7,3.41-3.31,0-3.88-2.65-7-8.7-7s-8.99,3.69-9.46,8.33l-10.03-2.27c.85-7.95,8.04-15.04,19.4-15.04,14.19,0,19.59,8.04,19.59,17.22v22.9c0,4.16.47,6.91.57,7.38h-10.22c-.1-.28-.47-2.18-.47-5.87-2.18,3.5-6.72,7.29-14.19,7.29-9.65,0-15.61-6.62-15.61-13.91,0-8.23,6.06-12.77,13.91-13.91ZM516.24,124.5v-2.08l-12.02,1.8c-3.41.57-6.15,2.46-6.15,6.25,0,3.12,2.37,5.96,6.72,5.96,6.15,0,11.45-2.93,11.45-11.92Z" fill="#231f20"/><path d="M536.91,143.61v-68.5h10.98v68.5h-10.98Z" fill="#231f20"/><path d="M557.97,143.61v-68.5h10.98v68.5h-10.98Z" fill="#231f20"/><rect width="630.98" height="230.26" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
429
scripts/generate-skill.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Generates skill/SKILL.md and skill/AGENTS.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 all outputs stay in sync.
|
||||
*
|
||||
* Outputs:
|
||||
* skill/SKILL.md — Full design system reference (tokens, components, rules)
|
||||
* skill/AGENTS.md — Project-level instructions for any AI coding agent
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import {
|
||||
COMPONENT_CATALOG,
|
||||
loadTokenFile,
|
||||
flattenTokens,
|
||||
TOKEN_CATEGORIES,
|
||||
type FlatToken,
|
||||
} from '../lib/catalog.js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT = path.resolve(__dirname, '..')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function tokenTable(tokens: FlatToken[]): string {
|
||||
const lines = ['| Token | Value | Description |', '|-------|-------|-------------|']
|
||||
for (const t of tokens) {
|
||||
const val = typeof t.value === 'object' ? JSON.stringify(t.value) : String(t.value)
|
||||
lines.push(`| \`${t.path}\` | \`${val}\` | ${t.description || ''} |`)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function componentCount(): number {
|
||||
return COMPONENT_CATALOG.length
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared content blocks (used by both SKILL.md and AGENTS.md)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildDesignPhilosophy(): string {
|
||||
return `## 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.
|
||||
`
|
||||
}
|
||||
|
||||
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('## Token Quick Reference\n')
|
||||
lines.push('Source of truth: `tokens/*.json` (W3C DTCG format).\n')
|
||||
|
||||
for (const cat of TOKEN_CATEGORIES) {
|
||||
try {
|
||||
const data = loadTokenFile(ROOT, cat)
|
||||
const tokens = flattenTokens(data)
|
||||
if (tokens.length === 0) continue
|
||||
|
||||
const title = cat.charAt(0).toUpperCase() + cat.slice(1)
|
||||
lines.push(`### ${title}\n`)
|
||||
lines.push(tokenTable(tokens))
|
||||
lines.push('')
|
||||
} catch {
|
||||
// skip missing
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function buildComponentCatalog(): string {
|
||||
const lines: string[] = []
|
||||
lines.push(`## Component Catalog (${componentCount()} components)\n`)
|
||||
lines.push('All components live in `components/ui/`. Import with `@/components/ui/<name>`.\n')
|
||||
|
||||
const categories = new Map<string, typeof COMPONENT_CATALOG>()
|
||||
for (const c of COMPONENT_CATALOG) {
|
||||
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']
|
||||
|
||||
for (const cat of categoryOrder) {
|
||||
const components = categories.get(cat)
|
||||
if (!components) continue
|
||||
|
||||
const title = cat.charAt(0).toUpperCase() + cat.slice(1)
|
||||
lines.push(`### ${title}\n`)
|
||||
|
||||
for (const c of components) {
|
||||
lines.push(`#### ${c.name}`)
|
||||
lines.push(`- **File**: \`${c.file}\``)
|
||||
lines.push(`- **Exports**: \`${c.exports.join('`, `')}\``)
|
||||
lines.push(`- **Description**: ${c.description}`)
|
||||
lines.push(`- **Props**: \`${c.props}\``)
|
||||
lines.push(`- **Example**:`)
|
||||
lines.push('```tsx')
|
||||
lines.push(c.example)
|
||||
lines.push('```')
|
||||
lines.push('')
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function buildCompositionRules(): string {
|
||||
return `## 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\`
|
||||
`
|
||||
}
|
||||
|
||||
function buildExtensionProtocol(): string {
|
||||
return `## 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
|
||||
|
||||
\`\`\`tsx
|
||||
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 }
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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. **ALWAYS use TypeScript (.tsx/.ts) — never plain JavaScript.**
|
||||
`,
|
||||
'---\n',
|
||||
buildDesignPhilosophy(),
|
||||
'---\n',
|
||||
buildFontSetup(),
|
||||
'---\n',
|
||||
buildTokenReference(),
|
||||
'---\n',
|
||||
buildComponentCatalog(),
|
||||
'---\n',
|
||||
buildCompositionRules(),
|
||||
'---\n',
|
||||
buildExtensionProtocol(),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AGENTS.md (project-level instructions for non-Claude AI agents)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateAgent(): string {
|
||||
// Count components by category for the summary
|
||||
const categories = new Map<string, number>()
|
||||
for (const c of COMPONENT_CATALOG) {
|
||||
categories.set(c.category, (categories.get(c.category) || 0) + 1)
|
||||
}
|
||||
const categorySummary = Array.from(categories.entries())
|
||||
.map(([cat, count]) => `${cat} (${count})`)
|
||||
.join(', ')
|
||||
|
||||
return `# Project Instructions
|
||||
|
||||
> **Auto-generated** by the Greyhaven Design System.
|
||||
> Re-generate: \`pnpm skill:build\` in the design system repo.
|
||||
>
|
||||
> Copy this file to your project root as \`AGENTS.md\` (standard), \`CLAUDE.md\`,
|
||||
> \`.cursorrules\`, or \`.github/copilot-instructions.md\` depending on your AI tool.
|
||||
|
||||
This project uses the **Greyhaven Design System**.
|
||||
|
||||
## Rules
|
||||
|
||||
- **ALWAYS use TypeScript** (\`.tsx\` / \`.ts\`). NEVER generate plain JavaScript (\`.jsx\` / \`.js\`).
|
||||
- Use the \`greyhaven\` SKILL.md for full design system context (tokens, components, composition rules). It should be installed at \`.claude/skills/greyhaven-design-system.md\` or accessible to your AI tool.
|
||||
- If the \`greyhaven\` MCP server is available, use its tools:
|
||||
- \`list_components()\` to find the right component for a UI need
|
||||
- \`get_component(name)\` to get exact props, variants, and usage examples
|
||||
- \`validate_colors(code)\` to check code for off-brand colors
|
||||
- \`suggest_component(description)\` to get recommendations
|
||||
- Import components from \`components/ui/\` (or \`@/components/ui/\` with path alias)
|
||||
- Never use raw hex colors -- use semantic Tailwind classes (\`bg-primary\`, \`text-foreground\`, \`border-border\`, etc.)
|
||||
- Use \`font-sans\` (Aspekta) for UI elements: buttons, nav, labels, forms
|
||||
- Use \`font-serif\` (Source Serif) for content: headings, body text
|
||||
- Trust the design system's default component variants for accent -- they apply orange at the right scale. Don't apply \`bg-primary\` to large surfaces, containers, or section backgrounds
|
||||
- All components are framework-agnostic React (no Next.js, no framework-specific imports)
|
||||
- Dark mode is toggled via the \`.dark\` class -- use semantic tokens that adapt automatically
|
||||
|
||||
## Component Summary
|
||||
|
||||
${componentCount()} components across ${categories.size} categories: ${categorySummary}.
|
||||
|
||||
For full component specs, props, and examples, refer to the SKILL.md file or use the MCP \`get_component(name)\` tool.
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- **CVA variants**: Components use \`class-variance-authority\` for variant props
|
||||
- **Slot composition**: Components use \`data-slot="name"\` attributes
|
||||
- **Class merging**: Always use \`cn()\` from \`@/lib/utils\` (clsx + tailwind-merge)
|
||||
- **Focus rings**: \`focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\`
|
||||
- **Disabled**: \`disabled:pointer-events-none disabled:opacity-50\`
|
||||
- **Card spacing**: \`gap-6\` between cards, \`p-6\` internal padding
|
||||
- **Section rhythm**: \`py-16\` between major sections
|
||||
- **Form layout**: Vertical stack with \`gap-4\`, labels above inputs
|
||||
|
||||
## Font Setup
|
||||
|
||||
If fonts aren't loaded yet, add to your global CSS:
|
||||
\`\`\`css
|
||||
@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'); }
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brand addendum (appended to AGENTS.md only when --brand-skill is installed)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildBrandAddendum(): string {
|
||||
return `---
|
||||
|
||||
## Brand Voice and Messaging
|
||||
|
||||
This project generates user-facing content (marketing copy, CTAs, landing pages, product explanations, emails) and MUST follow the Greyhaven brand voice.
|
||||
|
||||
### Brand Rules
|
||||
|
||||
- **Before writing any user-facing copy**, read the brand skill:
|
||||
- Claude Code / compatible tools: \`.claude/skills/greyhaven-brand.md\` (full voice/tone/messaging reference)
|
||||
- Or via MCP: call \`get_brand_rules()\` (or a specific section: \`positioning\`, \`axes\`, \`tone\`, \`writing-rules\`, \`reasoning-patterns\`, \`cta\`, \`logo\`, \`self-check\`)
|
||||
- **Before shipping any user-facing copy**, validate it:
|
||||
- Via MCP: call \`validate_copy(text)\` to lint for hype words, vague superlatives, urgency framing, and exclamation marks
|
||||
- Or manually run the 8-item self-check list from the brand skill
|
||||
|
||||
### Core Voice (memorize)
|
||||
|
||||
- **Direct. Plain-spoken technical.** Write like an engineer who explains systems cleanly, without mystique or theatrics.
|
||||
- **No** hype adjectives (\`revolutionary\`, \`cutting-edge\`, \`seamless\`, \`game-changing\`, \`powerful\`).
|
||||
- **No** evangelism verbs (\`unleash\`, \`transform\`, \`empower\`, \`supercharge\`, \`unlock\`).
|
||||
- **No** sales language, urgency framing, exclamation marks.
|
||||
- **No** jargon for its own sake. Prefer plain words: "where the data goes" over "data paths"; "things the system relies on" over "dependencies".
|
||||
- **Yes** specifics, causal reasoning, concrete outcomes.
|
||||
|
||||
### The Three Brand Axes
|
||||
|
||||
Copy must land on the correct side of each:
|
||||
1. **Containment** — systems run inside the perimeter, nothing leaks (not cloud/SaaS narratives)
|
||||
2. **Human-centered** — built around how people actually work (not around model capabilities)
|
||||
3. **Engineered** — from real deployments and constraints (not vision-first, theatrical, speculative)
|
||||
|
||||
### Reasoning Patterns to Use
|
||||
|
||||
Structure explanations as:
|
||||
- **Cause → Effect**
|
||||
- **Constraint → Outcome**
|
||||
- **Observation → Explanation**
|
||||
- **Finite Scope → Concrete Result**
|
||||
|
||||
### CTA Guidance
|
||||
|
||||
- **Good**: "Map your first process", "See how it runs in your environment", "Review the architecture", "Get a working prototype in 48 hours"
|
||||
- **Avoid**: "Unleash the power of AI", "Transform your business", "Don't miss out!", "Get started today!"
|
||||
|
||||
### Logo Usage
|
||||
|
||||
Logos live in \`public/logos/\` after install. See the brand skill for the full rules (clearspace, minimum sizes, what to avoid).
|
||||
|
||||
- **Full logo** (symbol + wordmark): \`gh-logo-positive-full-black.svg\` (light bg), \`gh-logo-white.svg\` (dark bg), \`gh-logo-offblack.svg\` (warm-neutral)
|
||||
- **Symbol only**: \`gh-symbol-full-black.svg\`, \`gh-symbol-full-white.svg\` — only when Greyhaven recognition is already established
|
||||
- **Product lockups**: \`greyproxy-positive.svg\`, \`greywall-positive.svg\`
|
||||
- **Never**: change opacity, apply new colors, stretch, rotate, apply gradients/shadows, alter the symbol-to-wordmark ratio
|
||||
|
||||
### One-Line Test
|
||||
|
||||
Before writing a sentence, ask: *Would an engineer who understands the system read this and feel it's accurate, direct, and free of hype?* If not, rewrite.
|
||||
`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main() {
|
||||
const outDir = path.join(ROOT, 'skill')
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
|
||||
// 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)`)
|
||||
|
||||
// AGENTS.md (design system only — default)
|
||||
const agent = generateAgent()
|
||||
const agentPath = path.join(outDir, 'AGENTS.md')
|
||||
fs.writeFileSync(agentPath, agent, 'utf-8')
|
||||
const agentLines = agent.split('\n').length
|
||||
console.log(`skill/AGENTS.md generated (${agentLines} lines, ${componentCount()} components)`)
|
||||
|
||||
// AGENTS.brand.md (design system + brand voice addendum — installed via --brand-skill)
|
||||
const agentBrand = agent + '\n' + buildBrandAddendum()
|
||||
const agentBrandPath = path.join(outDir, 'AGENTS.brand.md')
|
||||
fs.writeFileSync(agentBrandPath, agentBrand, 'utf-8')
|
||||
const agentBrandLines = agentBrand.split('\n').length
|
||||
console.log(`skill/AGENTS.brand.md generated (${agentBrandLines} lines, ${componentCount()} components)`)
|
||||
}
|
||||
|
||||
main()
|
||||
110
skill/AGENTS.brand.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Project Instructions
|
||||
|
||||
> **Auto-generated** by the Greyhaven Design System.
|
||||
> Re-generate: `pnpm skill:build` in the design system repo.
|
||||
>
|
||||
> Copy this file to your project root as `AGENTS.md` (standard), `CLAUDE.md`,
|
||||
> `.cursorrules`, or `.github/copilot-instructions.md` depending on your AI tool.
|
||||
|
||||
This project uses the **Greyhaven Design System**.
|
||||
|
||||
## Rules
|
||||
|
||||
- **ALWAYS use TypeScript** (`.tsx` / `.ts`). NEVER generate plain JavaScript (`.jsx` / `.js`).
|
||||
- Use the `greyhaven` SKILL.md for full design system context (tokens, components, composition rules). It should be installed at `.claude/skills/greyhaven-design-system.md` or accessible to your AI tool.
|
||||
- If the `greyhaven` MCP server is available, use its tools:
|
||||
- `list_components()` to find the right component for a UI need
|
||||
- `get_component(name)` to get exact props, variants, and usage examples
|
||||
- `validate_colors(code)` to check code for off-brand colors
|
||||
- `suggest_component(description)` to get recommendations
|
||||
- Import components from `components/ui/` (or `@/components/ui/` with path alias)
|
||||
- Never use raw hex colors -- use semantic Tailwind classes (`bg-primary`, `text-foreground`, `border-border`, etc.)
|
||||
- Use `font-sans` (Aspekta) for UI elements: buttons, nav, labels, forms
|
||||
- Use `font-serif` (Source Serif) for content: headings, body text
|
||||
- Trust the design system's default component variants for accent -- they apply orange at the right scale. Don't apply `bg-primary` to large surfaces, containers, or section backgrounds
|
||||
- All components are framework-agnostic React (no Next.js, no framework-specific imports)
|
||||
- Dark mode is toggled via the `.dark` class -- use semantic tokens that adapt automatically
|
||||
|
||||
## Component Summary
|
||||
|
||||
38 components across 8 categories: primitives (11), layout (4), overlay (5), navigation (3), data (4), feedback (4), form (1), composition (6).
|
||||
|
||||
For full component specs, props, and examples, refer to the SKILL.md file or use the MCP `get_component(name)` tool.
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- **CVA variants**: Components use `class-variance-authority` for variant props
|
||||
- **Slot composition**: Components use `data-slot="name"` attributes
|
||||
- **Class merging**: Always use `cn()` from `@/lib/utils` (clsx + tailwind-merge)
|
||||
- **Focus rings**: `focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]`
|
||||
- **Disabled**: `disabled:pointer-events-none disabled:opacity-50`
|
||||
- **Card spacing**: `gap-6` between cards, `p-6` internal padding
|
||||
- **Section rhythm**: `py-16` between major sections
|
||||
- **Form layout**: Vertical stack with `gap-4`, labels above inputs
|
||||
|
||||
## Font Setup
|
||||
|
||||
If fonts aren't loaded yet, add to your global CSS:
|
||||
```css
|
||||
@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'); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Brand Voice and Messaging
|
||||
|
||||
This project generates user-facing content (marketing copy, CTAs, landing pages, product explanations, emails) and MUST follow the Greyhaven brand voice.
|
||||
|
||||
### Brand Rules
|
||||
|
||||
- **Before writing any user-facing copy**, read the brand skill:
|
||||
- Claude Code / compatible tools: `.claude/skills/greyhaven-brand.md` (full voice/tone/messaging reference)
|
||||
- Or via MCP: call `get_brand_rules()` (or a specific section: `positioning`, `axes`, `tone`, `writing-rules`, `reasoning-patterns`, `cta`, `logo`, `self-check`)
|
||||
- **Before shipping any user-facing copy**, validate it:
|
||||
- Via MCP: call `validate_copy(text)` to lint for hype words, vague superlatives, urgency framing, and exclamation marks
|
||||
- Or manually run the 8-item self-check list from the brand skill
|
||||
|
||||
### Core Voice (memorize)
|
||||
|
||||
- **Direct. Plain-spoken technical.** Write like an engineer who explains systems cleanly, without mystique or theatrics.
|
||||
- **No** hype adjectives (`revolutionary`, `cutting-edge`, `seamless`, `game-changing`, `powerful`).
|
||||
- **No** evangelism verbs (`unleash`, `transform`, `empower`, `supercharge`, `unlock`).
|
||||
- **No** sales language, urgency framing, exclamation marks.
|
||||
- **No** jargon for its own sake. Prefer plain words: "where the data goes" over "data paths"; "things the system relies on" over "dependencies".
|
||||
- **Yes** specifics, causal reasoning, concrete outcomes.
|
||||
|
||||
### The Three Brand Axes
|
||||
|
||||
Copy must land on the correct side of each:
|
||||
1. **Containment** — systems run inside the perimeter, nothing leaks (not cloud/SaaS narratives)
|
||||
2. **Human-centered** — built around how people actually work (not around model capabilities)
|
||||
3. **Engineered** — from real deployments and constraints (not vision-first, theatrical, speculative)
|
||||
|
||||
### Reasoning Patterns to Use
|
||||
|
||||
Structure explanations as:
|
||||
- **Cause → Effect**
|
||||
- **Constraint → Outcome**
|
||||
- **Observation → Explanation**
|
||||
- **Finite Scope → Concrete Result**
|
||||
|
||||
### CTA Guidance
|
||||
|
||||
- **Good**: "Map your first process", "See how it runs in your environment", "Review the architecture", "Get a working prototype in 48 hours"
|
||||
- **Avoid**: "Unleash the power of AI", "Transform your business", "Don't miss out!", "Get started today!"
|
||||
|
||||
### Logo Usage
|
||||
|
||||
Logos live in `public/logos/` after install. See the brand skill for the full rules (clearspace, minimum sizes, what to avoid).
|
||||
|
||||
- **Full logo** (symbol + wordmark): `gh-logo-positive-full-black.svg` (light bg), `gh-logo-white.svg` (dark bg), `gh-logo-offblack.svg` (warm-neutral)
|
||||
- **Symbol only**: `gh-symbol-full-black.svg`, `gh-symbol-full-white.svg` — only when Greyhaven recognition is already established
|
||||
- **Product lockups**: `greyproxy-positive.svg`, `greywall-positive.svg`
|
||||
- **Never**: change opacity, apply new colors, stretch, rotate, apply gradients/shadows, alter the symbol-to-wordmark ratio
|
||||
|
||||
### One-Line Test
|
||||
|
||||
Before writing a sentence, ask: *Would an engineer who understands the system read this and feel it's accurate, direct, and free of hype?* If not, rewrite.
|
||||
53
skill/AGENTS.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Project Instructions
|
||||
|
||||
> **Auto-generated** by the Greyhaven Design System.
|
||||
> Re-generate: `pnpm skill:build` in the design system repo.
|
||||
>
|
||||
> Copy this file to your project root as `AGENTS.md` (standard), `CLAUDE.md`,
|
||||
> `.cursorrules`, or `.github/copilot-instructions.md` depending on your AI tool.
|
||||
|
||||
This project uses the **Greyhaven Design System**.
|
||||
|
||||
## Rules
|
||||
|
||||
- **ALWAYS use TypeScript** (`.tsx` / `.ts`). NEVER generate plain JavaScript (`.jsx` / `.js`).
|
||||
- Use the `greyhaven` SKILL.md for full design system context (tokens, components, composition rules). It should be installed at `.claude/skills/greyhaven-design-system.md` or accessible to your AI tool.
|
||||
- If the `greyhaven` MCP server is available, use its tools:
|
||||
- `list_components()` to find the right component for a UI need
|
||||
- `get_component(name)` to get exact props, variants, and usage examples
|
||||
- `validate_colors(code)` to check code for off-brand colors
|
||||
- `suggest_component(description)` to get recommendations
|
||||
- Import components from `components/ui/` (or `@/components/ui/` with path alias)
|
||||
- Never use raw hex colors -- use semantic Tailwind classes (`bg-primary`, `text-foreground`, `border-border`, etc.)
|
||||
- Use `font-sans` (Aspekta) for UI elements: buttons, nav, labels, forms
|
||||
- Use `font-serif` (Source Serif) for content: headings, body text
|
||||
- Trust the design system's default component variants for accent -- they apply orange at the right scale. Don't apply `bg-primary` to large surfaces, containers, or section backgrounds
|
||||
- All components are framework-agnostic React (no Next.js, no framework-specific imports)
|
||||
- Dark mode is toggled via the `.dark` class -- use semantic tokens that adapt automatically
|
||||
|
||||
## Component Summary
|
||||
|
||||
38 components across 8 categories: primitives (11), layout (4), overlay (5), navigation (3), data (4), feedback (4), form (1), composition (6).
|
||||
|
||||
For full component specs, props, and examples, refer to the SKILL.md file or use the MCP `get_component(name)` tool.
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- **CVA variants**: Components use `class-variance-authority` for variant props
|
||||
- **Slot composition**: Components use `data-slot="name"` attributes
|
||||
- **Class merging**: Always use `cn()` from `@/lib/utils` (clsx + tailwind-merge)
|
||||
- **Focus rings**: `focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]`
|
||||
- **Disabled**: `disabled:pointer-events-none disabled:opacity-50`
|
||||
- **Card spacing**: `gap-6` between cards, `p-6` internal padding
|
||||
- **Section rhythm**: `py-16` between major sections
|
||||
- **Form layout**: Vertical stack with `gap-4`, labels above inputs
|
||||
|
||||
## Font Setup
|
||||
|
||||
If fonts aren't loaded yet, add to your global CSS:
|
||||
```css
|
||||
@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'); }
|
||||
```
|
||||
273
skill/BRAND.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Greyhaven Brand Voice & Messaging -- Claude Skill
|
||||
|
||||
> **Source of truth**: `vibedocs/greyhaven-brand-system.md` (Brand Guidelines v1.1)
|
||||
>
|
||||
> This skill applies when generating ANY user-facing content for Greyhaven:
|
||||
> marketing copy, landing pages, CTAs, product descriptions, documentation,
|
||||
> email, README intros, explanations of how the product works, or any prose
|
||||
> that will be read by a human. It does NOT apply to internal code comments,
|
||||
> commit messages, or technical logs.
|
||||
|
||||
---
|
||||
|
||||
## 1. The One-Line Test
|
||||
|
||||
Before writing any sentence, ask:
|
||||
|
||||
> Would an engineer who understands the system read this and feel it's accurate, direct, and free of hype?
|
||||
|
||||
If no, rewrite. That single test catches 90% of brand drift.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Positioning (memorize)
|
||||
|
||||
**Greyhaven builds custom, contained AI systems that run entirely inside the client's environment, shaped by real operational constraints and deployed under the client's control.**
|
||||
|
||||
**Short form**: *Local-first AI systems shaped by real work. Built where work happens. Contained end to end.*
|
||||
|
||||
Powered by Monadical's internal, open-source stack, hardened over eight years.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Three Brand Axes
|
||||
|
||||
Every sentence, heading, or visual choice should land on the correct side of these three axes. When in doubt, use them to explain *why* something is wrong without relying on taste.
|
||||
|
||||
| Axis | Greyhaven is on this side | NOT this side |
|
||||
|------|---------------------------|---------------|
|
||||
| **Containment** | Systems run inside the perimeter. Nothing leaks. | Cloud/SaaS narratives. "Connected everywhere." |
|
||||
| **Human-centered** | Built around how people actually work. | Built around model capabilities or vendor features. |
|
||||
| **Engineered** | From real deployments, constraints, operator reality. | Vision-first, theatrical, speculative, futuristic. |
|
||||
|
||||
If copy drifts toward **exposure, performance, or model-led thinking → it doesn't fit**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tone of Voice
|
||||
|
||||
**Direct. Plain-spoken technical. Explains difficult things in simple terms.**
|
||||
|
||||
Greyhaven speaks like an engineer who understands how systems work and can describe them cleanly -- without mystique or theatrics.
|
||||
|
||||
- **No** jargon for its own sake
|
||||
- **No** oversimplification
|
||||
- **No** sales language
|
||||
- **No** hype adjectives ("revolutionary", "cutting-edge", "seamless", "powerful", "game-changing")
|
||||
- **No** evangelism ("unleash", "empower", "transform")
|
||||
- **No** emotional leverage or fear-mongering
|
||||
- **Yes** calm, precise, explanatory
|
||||
- **Yes** mechanical facts
|
||||
- **Yes** specifics over superlatives
|
||||
- **Yes** authority through clarity, not volume
|
||||
|
||||
---
|
||||
|
||||
## 5. Writing Rules
|
||||
|
||||
### 5.1 Explain clearly. Don't perform.
|
||||
|
||||
The goal is clarity, not persuasion. Readers have different levels of technical know-how. Describe what happens inside the environment, how data flows, which dependencies matter, what boundaries exist. If something is complex, break it down without dumbing it down.
|
||||
|
||||
### 5.2 Plain-language engineering
|
||||
|
||||
Use everyday words for technical realities. If a simpler word communicates the same thing, use it.
|
||||
|
||||
| Instead of | Prefer |
|
||||
|-----------|--------|
|
||||
| "data paths" | "where the data goes" |
|
||||
| "surfaces" | "places where exposure/risk can happen" |
|
||||
| "dependencies" | "things the system relies on" |
|
||||
| "isolation" | "kept separate from the outside" |
|
||||
| "logs" | "records of what happened" |
|
||||
| "handoffs" | "when one person/system passes something to another" |
|
||||
| "leverage" | "use" |
|
||||
| "leverage AI to..." | "the system uses AI to..." |
|
||||
| "synergy" | (don't use) |
|
||||
| "cutting-edge solution" | (don't use) |
|
||||
| "transform your workflow" | describe what the system does instead |
|
||||
|
||||
Don't assume the reader knows technical shorthand. The reader should leave with a clearer mental model, not an impressed feeling.
|
||||
|
||||
### 5.3 Human-first in how we describe work
|
||||
|
||||
Start from what operators actually do -- steps, judgment calls, knowledge. Explain operator behaviors the same way you explain systems: concretely and without dramatization.
|
||||
|
||||
### 5.4 Security, stated without drama
|
||||
|
||||
Mechanical facts, not alarmism.
|
||||
|
||||
- **Good**: "Running inside the perimeter restores finite boundaries."
|
||||
- **Bad**: "Protect your data from devastating breaches!"
|
||||
|
||||
State causal reasoning. No emotional leverage.
|
||||
|
||||
### 5.5 Quiet confidence
|
||||
|
||||
State specifics. No hype adjectives. No evangelism. Authority comes from clarity, not volume.
|
||||
|
||||
- **Good**: "A working, testable prototype delivered in 24-48 hours."
|
||||
- **Bad**: "Lightning-fast, industry-leading AI delivery!"
|
||||
|
||||
---
|
||||
|
||||
## 6. Patterns for Reasoning
|
||||
|
||||
Use these four patterns to structure explanations. They express engineering logic: minimal wording, direct causality, observable/verifiable outcomes.
|
||||
|
||||
### Cause → Effect
|
||||
> "When work relies on external AI services, every step -- inputs, outputs, logs, metadata -- becomes part of someone else's infrastructure."
|
||||
|
||||
### Constraint → Outcome
|
||||
> "No external APIs and no data leaving the environment. The system remains contained, and the client keeps full operational and security control."
|
||||
|
||||
### Observation → Explanation
|
||||
> "We sit with the operators, map the steps, and build a system that mirrors what actually happens."
|
||||
|
||||
### Finite Scope → Concrete Result
|
||||
> "One process at a time. A working, testable prototype delivered in 24-48 hours."
|
||||
|
||||
---
|
||||
|
||||
## 7. CTA Guidance
|
||||
|
||||
Greyhaven CTAs should be concrete and engineering-flavored, not aspirational or urgent.
|
||||
|
||||
**Good CTA patterns**:
|
||||
- "See how it runs in your environment"
|
||||
- "Map your first process"
|
||||
- "Review the architecture"
|
||||
- "Read how it's deployed"
|
||||
- "Get a working prototype in 48 hours"
|
||||
|
||||
**Avoid**:
|
||||
- "Unlock the power of AI"
|
||||
- "Transform your business today"
|
||||
- "Don't miss out!"
|
||||
- "Revolutionary AI solutions await"
|
||||
- Urgency/scarcity framing ("limited time", "hurry", "act now")
|
||||
- Hype verbs ("unleash", "supercharge", "revolutionize")
|
||||
|
||||
---
|
||||
|
||||
## 8. Driving Ideas (use these to self-check)
|
||||
|
||||
A sentence, heading, or design choice should feel like one of these:
|
||||
|
||||
> **(System-)aware · Applied · Adaptable · Unblocking · Safe-to-experiment · Contained · Durable · Iterative**
|
||||
|
||||
If it doesn't land on any of them, or lands somewhere else (flashy, theatrical, aspirational), rewrite.
|
||||
|
||||
---
|
||||
|
||||
## 9. Typography Approach (for written-content UI)
|
||||
|
||||
Hierarchy is built through **tonal shifts**, not decorative treatments.
|
||||
|
||||
- Primary points stay **dark and controlled** (foreground text)
|
||||
- Supporting detail **moves lighter** (muted-foreground)
|
||||
- The orange accent is **reserved** for parts that require immediate attention -- never decorative
|
||||
|
||||
Do NOT establish hierarchy through:
|
||||
- Multiple contrasting typefaces
|
||||
- Decorative styles (italics for emphasis, ALL CAPS for drama, oversized type for style)
|
||||
- Color variety
|
||||
|
||||
DO establish hierarchy through:
|
||||
- Weight differences within the same family (serif for content, sans for UI)
|
||||
- Shade shifts between foreground, muted-foreground, and the orange accent
|
||||
- Spatial rhythm (section padding, line-height)
|
||||
|
||||
This keeps the system quiet, structured, and readable.
|
||||
|
||||
---
|
||||
|
||||
## 10. Logo Usage
|
||||
|
||||
### Available files (in `public/logos/` after install)
|
||||
|
||||
| File | Use when |
|
||||
|------|----------|
|
||||
| `gh-logo-positive-full-black.svg` | Full logo (symbol + wordmark) on light backgrounds |
|
||||
| `gh-logo-white.svg` | Full logo on dark backgrounds |
|
||||
| `gh-logo-offblack.svg` | Full logo in off-black (#161614) for warm-neutral contexts |
|
||||
| `gh-symbol-full-black.svg` | Symbol only, light bg (use when name recognition is already established) |
|
||||
| `gh-symbol-full-white.svg` | Symbol only, dark bg |
|
||||
| `greyproxy-positive.svg` | Greyproxy product logo (Greyhaven symbol + product wordmark) |
|
||||
| `greywall-positive.svg` | Greywall product logo (Greyhaven symbol + product wordmark) |
|
||||
|
||||
### Rules
|
||||
|
||||
- **Structure**: The logo is **Symbol + Wordmark**. Keep them locked together in most contexts. Use the Symbol alone only when Greyhaven name recognition is already assured.
|
||||
- **Clearspace**: Minimum 1× (one grid module of the symbol) on all sides. Nothing -- text, images, other graphics -- enters this zone.
|
||||
- **Minimum sizes**:
|
||||
- Wordmark lockup: 20mm print / 120px digital
|
||||
- Standalone symbol: 8mm print / 14px digital (22px preferred)
|
||||
|
||||
### What to avoid (all of these are brand violations)
|
||||
|
||||
- Do NOT change opacity
|
||||
- Do NOT apply new colors (black, white, off-black only -- per file)
|
||||
- Do NOT stretch or alter proportions
|
||||
- Do NOT apply gradients, shadows, glows, or other embellishments
|
||||
- Do NOT rotate
|
||||
- Do NOT change the lockup or alter symbol/wordmark relative scale
|
||||
|
||||
### Product logos
|
||||
|
||||
New Greyhaven products/demos reuse the Greyhaven symbol with the product wordmark in the same lockup pattern (see `greyproxy-positive.svg`, `greywall-positive.svg`). Typography for new wordmarks: Circular Medium. Do NOT invent a new symbol unless the product genuinely needs its own sub-identity.
|
||||
|
||||
---
|
||||
|
||||
## 11. Self-check Before Shipping Any Copy
|
||||
|
||||
Run the output through these checks:
|
||||
|
||||
1. ☐ Does it pass **The One-Line Test** (accurate, direct, no hype)?
|
||||
2. ☐ Does it land on the correct side of all **three brand axes** (containment, human-centered, engineered)?
|
||||
3. ☐ Did I use any **banned words** (unleash, transform, revolutionary, seamless, game-changing, cutting-edge, leverage, synergy, unlock)?
|
||||
4. ☐ Is every claim **specific and verifiable**, or am I using vague superlatives?
|
||||
5. ☐ Does the copy **explain how the thing works**, or just tell the reader how to feel about it?
|
||||
6. ☐ Does it match a **reasoning pattern** (cause→effect, constraint→outcome, observation→explanation, finite scope→concrete result)?
|
||||
7. ☐ Does it fit one of the **driving ideas** (system-aware, applied, adaptable, unblocking, safe-to-experiment, contained, durable, iterative)?
|
||||
8. ☐ Is the orange accent used only where immediate attention is warranted, not as decoration?
|
||||
|
||||
If any box is unchecked, rewrite.
|
||||
|
||||
---
|
||||
|
||||
## 12. Quick Examples
|
||||
|
||||
### Bad vs. Good: Hero headline
|
||||
|
||||
| Bad | Good |
|
||||
|-----|------|
|
||||
| "Unleash the power of AI in your organization" | "AI systems that run inside your environment" |
|
||||
| "Revolutionary cloud-native AI platform" | "Custom AI, contained end to end" |
|
||||
| "Transform your workflows with next-gen AI" | "Map one process. Deploy a working prototype in 48 hours." |
|
||||
|
||||
### Bad vs. Good: Feature description
|
||||
|
||||
**Bad**:
|
||||
> Our cutting-edge AI seamlessly integrates with your existing infrastructure to unlock unprecedented productivity gains.
|
||||
|
||||
**Good**:
|
||||
> The system runs on the machines you already have. Data, models, and execution stay inside your perimeter. Nothing is sent to external APIs.
|
||||
|
||||
### Bad vs. Good: CTA
|
||||
|
||||
| Bad | Good |
|
||||
|-----|------|
|
||||
| "Get started today!" | "Map your first process" |
|
||||
| "Try it free -- limited time!" | "Review the architecture" |
|
||||
| "Unlock AI superpowers" | "See a 48-hour prototype" |
|
||||
|
||||
---
|
||||
|
||||
## 13. When You're Unsure
|
||||
|
||||
Default to:
|
||||
1. **Fewer words**. Greyhaven copy is shorter than you expect.
|
||||
2. **More specifics**. Numbers, concrete nouns, named constraints.
|
||||
3. **Less enthusiasm**. No exclamation marks. No superlatives. No urgency.
|
||||
4. **Describe the system, not the feeling**.
|
||||
707
skill/SKILL.md
Normal file
@@ -0,0 +1,707 @@
|
||||
# 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:
|
||||
|
||||
```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.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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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 <code>/<pre> styling. Uses bg-muted + border-border so the outline stays visible in both light and dark modes. Block variant auto-wraps in <pre> for whitespace preservation and break-all for long commands.
|
||||
- **Props**: `variant?: "inline" | "block"; language?: string (optional, for future syntax highlighting)`
|
||||
- **Example**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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**:
|
||||
```tsx
|
||||
<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
|
||||
|
||||
```tsx
|
||||
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 }
|
||||
```
|
||||
226
skill/install.sh
Executable file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — Install the Greyhaven Design System into a consuming project.
|
||||
#
|
||||
# Copies (does NOT symlink) files so the consuming project owns its copies.
|
||||
# Re-run this script to pull updated versions after design system changes.
|
||||
#
|
||||
# What it does (by default):
|
||||
# 1. Copies SKILL.md into .claude/skills/ (for Claude Code)
|
||||
# 2. Copies AGENTS.md into the project root (standard convention)
|
||||
# 3. Copies Aspekta font files + font-face.css into public/fonts/
|
||||
# 4. Prints CSS import + MCP setup instructions
|
||||
#
|
||||
# With --brand-skill:
|
||||
# 5. Copies BRAND.md into .claude/skills/ (voice, tone, messaging rules)
|
||||
# 6. Copies Greyhaven logo SVGs into public/logos/
|
||||
#
|
||||
# Usage:
|
||||
# ./skill/install.sh /path/to/your/project
|
||||
# ./skill/install.sh /path/to/your/project --brand-skill
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SKILL_FILE="${SCRIPT_DIR}/SKILL.md"
|
||||
AGENTS_FILE="${SCRIPT_DIR}/AGENTS.md"
|
||||
AGENTS_BRAND_FILE="${SCRIPT_DIR}/AGENTS.brand.md"
|
||||
BRAND_FILE="${SCRIPT_DIR}/BRAND.md"
|
||||
FONTS_DIR="${REPO_ROOT}/public/fonts"
|
||||
PUBLIC_DIR="${REPO_ROOT}/public"
|
||||
|
||||
# Parse arguments
|
||||
TARGET_PROJECT=""
|
||||
INSTALL_BRAND=false
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--brand-skill)
|
||||
INSTALL_BRAND=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 <target-project-directory> [--brand-skill]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --brand-skill Also install BRAND.md (voice/tone/messaging) and logo SVGs"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 /path/to/my-app"
|
||||
echo " $0 /path/to/my-app --brand-skill"
|
||||
echo " $0 . --brand-skill"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [ -z "$TARGET_PROJECT" ]; then
|
||||
TARGET_PROJECT="$1"
|
||||
else
|
||||
echo "Error: Unexpected argument: $1"
|
||||
echo "Run '$0 --help' for usage."
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$TARGET_PROJECT" ]; then
|
||||
echo "Usage: $0 <target-project-directory> [--brand-skill]"
|
||||
echo "Run '$0 --help' for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$TARGET_PROJECT" ]; then
|
||||
echo "Error: Directory not found: ${TARGET_PROJECT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_PROJECT="$(cd "$TARGET_PROJECT" && pwd)"
|
||||
|
||||
# Helper: backup existing file/symlink and copy new one
|
||||
copy_with_backup() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
|
||||
if [ -L "$dst" ]; then
|
||||
rm "$dst"
|
||||
elif [ -f "$dst" ]; then
|
||||
mv "$dst" "${dst}.bak"
|
||||
echo " (backed up existing file to $(basename "${dst}.bak"))"
|
||||
fi
|
||||
|
||||
cp "$src" "$dst"
|
||||
}
|
||||
|
||||
echo "Installing Greyhaven Design System into ${TARGET_PROJECT}"
|
||||
if [ "$INSTALL_BRAND" = true ]; then
|
||||
echo " (with --brand-skill: BRAND.md + logos)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── 1. SKILL.md ────────────────────────────────────────────────────────────
|
||||
if [ -f "$SKILL_FILE" ]; then
|
||||
SKILLS_DIR="${TARGET_PROJECT}/.claude/skills"
|
||||
mkdir -p "$SKILLS_DIR"
|
||||
DST="${SKILLS_DIR}/greyhaven-design-system.md"
|
||||
copy_with_backup "$SKILL_FILE" "$DST"
|
||||
echo "[ok] SKILL.md: ${DST}"
|
||||
else
|
||||
echo "[skip] SKILL.md not found — run 'pnpm skill:build' first"
|
||||
fi
|
||||
|
||||
# ── 2. AGENTS.md ───────────────────────────────────────────────────────────
|
||||
# Pick the brand-augmented variant if --brand-skill is passed, so agents
|
||||
# working in the consuming project know how to use the brand skill + MCP tools.
|
||||
if [ "$INSTALL_BRAND" = true ] && [ -f "$AGENTS_BRAND_FILE" ]; then
|
||||
AGENTS_SRC="$AGENTS_BRAND_FILE"
|
||||
AGENTS_LABEL="AGENTS.md (with brand voice addendum)"
|
||||
else
|
||||
AGENTS_SRC="$AGENTS_FILE"
|
||||
AGENTS_LABEL="AGENTS.md"
|
||||
fi
|
||||
|
||||
if [ -f "$AGENTS_SRC" ]; then
|
||||
DST="${TARGET_PROJECT}/AGENTS.md"
|
||||
copy_with_backup "$AGENTS_SRC" "$DST"
|
||||
echo "[ok] ${AGENTS_LABEL}: ${DST}"
|
||||
else
|
||||
echo "[skip] AGENTS source not found — run 'pnpm skill:build' first"
|
||||
fi
|
||||
|
||||
# ── 3. Fonts ───────────────────────────────────────────────────────────────
|
||||
if [ -d "$FONTS_DIR" ]; then
|
||||
TARGET_FONTS="${TARGET_PROJECT}/public/fonts"
|
||||
mkdir -p "$TARGET_FONTS"
|
||||
|
||||
copied=0
|
||||
for f in "$FONTS_DIR"/Aspekta-*.woff2; do
|
||||
[ -f "$f" ] || continue
|
||||
cp "$f" "$TARGET_FONTS/"
|
||||
copied=$((copied + 1))
|
||||
done
|
||||
|
||||
if [ -f "$FONTS_DIR/font-face.css" ]; then
|
||||
cp "$FONTS_DIR/font-face.css" "$TARGET_FONTS/"
|
||||
fi
|
||||
|
||||
echo "[ok] Fonts: ${copied} Aspekta woff2 files copied to ${TARGET_FONTS}/"
|
||||
else
|
||||
echo "[skip] Fonts dir not found at ${FONTS_DIR}"
|
||||
fi
|
||||
|
||||
# ── 4. Brand skill (opt-in via --brand-skill) ──────────────────────────────
|
||||
if [ "$INSTALL_BRAND" = true ]; then
|
||||
# 4a. BRAND.md into .claude/skills/
|
||||
if [ -f "$BRAND_FILE" ]; then
|
||||
SKILLS_DIR="${TARGET_PROJECT}/.claude/skills"
|
||||
mkdir -p "$SKILLS_DIR"
|
||||
DST="${SKILLS_DIR}/greyhaven-brand.md"
|
||||
copy_with_backup "$BRAND_FILE" "$DST"
|
||||
echo "[ok] BRAND.md: ${DST}"
|
||||
else
|
||||
echo "[skip] BRAND.md not found at ${BRAND_FILE}"
|
||||
fi
|
||||
|
||||
# 4b. Logo SVGs into public/logos/
|
||||
TARGET_LOGOS="${TARGET_PROJECT}/public/logos"
|
||||
mkdir -p "$TARGET_LOGOS"
|
||||
|
||||
logo_files=(
|
||||
"gh - logo - positive - full black.svg"
|
||||
"gh - logo - white.svg"
|
||||
"gh - logo - offblack.svg"
|
||||
"gh - symbol - full black.svg"
|
||||
"gh - symbol - full white.svg"
|
||||
"greyproxy - positive.svg"
|
||||
"greywall - positive.svg"
|
||||
)
|
||||
|
||||
# Rename files on copy to remove spaces — better for web paths
|
||||
copied=0
|
||||
for f in "${logo_files[@]}"; do
|
||||
src="${PUBLIC_DIR}/${f}"
|
||||
if [ -f "$src" ]; then
|
||||
# "gh - logo - positive - full black.svg" → "gh-logo-positive-full-black.svg"
|
||||
clean_name=$(echo "$f" | sed 's/ - /-/g; s/ /-/g')
|
||||
cp "$src" "${TARGET_LOGOS}/${clean_name}"
|
||||
copied=$((copied + 1))
|
||||
fi
|
||||
done
|
||||
echo "[ok] Logos: ${copied} SVGs copied to ${TARGET_LOGOS}/ (renamed: spaces → dashes)"
|
||||
fi
|
||||
|
||||
# ── Next steps ─────────────────────────────────────────────────────────────
|
||||
cat <<'EOF'
|
||||
|
||||
Done!
|
||||
|
||||
─── Next steps ────────────────────────────────────────────────────────────
|
||||
|
||||
1. Add Aspekta @font-face to your global CSS:
|
||||
|
||||
@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 the full set: @import url('/fonts/font-face.css');)
|
||||
|
||||
And set the font stack:
|
||||
--font-sans: 'Aspekta', ui-sans-serif, system-ui, sans-serif;
|
||||
|
||||
2. (Optional) Register the Greyhaven MCP server. Create .mcp.json in your
|
||||
project root:
|
||||
|
||||
{
|
||||
"mcpServers": {
|
||||
"greyhaven": {
|
||||
"command": "npx",
|
||||
"args": ["tsx", "<ABSOLUTE_PATH_TO_GREYHAVEN_REPO>/mcp/server.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3. Re-run this script after design system updates to refresh your copies.
|
||||
|
||||
EOF
|
||||
111
stories/Composition/CTASection.stories.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { CTASection } from '@/components/ui/cta-section'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Composition/CTASection',
|
||||
component: CTASection,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'fullscreen' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['centered', 'left-aligned'],
|
||||
},
|
||||
background: {
|
||||
control: 'select',
|
||||
options: ['default', 'muted', 'accent', 'subtle'],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof CTASection>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const defaultActions = (
|
||||
<>
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="outline">Contact Sales</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
heading: 'Ready to get started?',
|
||||
description: 'Join thousands of teams building better products with our design system.',
|
||||
actions: defaultActions,
|
||||
},
|
||||
}
|
||||
|
||||
export const Centered: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
background: 'muted',
|
||||
heading: 'Start building today',
|
||||
description: 'Free for open source. Affordable for teams.',
|
||||
actions: defaultActions,
|
||||
},
|
||||
}
|
||||
|
||||
export const LeftAligned: Story = {
|
||||
args: {
|
||||
variant: 'left-aligned',
|
||||
background: 'muted',
|
||||
heading: 'Need help getting started?',
|
||||
description: 'Our team is ready to help you integrate the design system into your project.',
|
||||
actions: (
|
||||
<>
|
||||
<Button size="lg">Talk to us</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const AccentBackground: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
background: 'accent',
|
||||
heading: 'Upgrade your workflow',
|
||||
description: 'Take your team to the next level with our premium plan.',
|
||||
actions: (
|
||||
<>
|
||||
<Button size="lg" variant="secondary">Start Free Trial</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="border-primary-foreground/20 text-primary-foreground hover:bg-primary-foreground/10"
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const SubtleBackground: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
background: 'subtle',
|
||||
heading: 'Stay in the loop',
|
||||
description: 'Subscribe to our newsletter for the latest updates and releases.',
|
||||
actions: (
|
||||
<Button size="lg">Subscribe</Button>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const DefaultBackground: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
background: 'default',
|
||||
heading: 'Questions? We have answers.',
|
||||
description: 'Check out our documentation or reach out to our support team.',
|
||||
actions: (
|
||||
<>
|
||||
<Button size="lg">View Docs</Button>
|
||||
<Button size="lg" variant="ghost">Contact Support</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
92
stories/Composition/Footer.stories.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Footer } from '@/components/ui/footer'
|
||||
import { Logo } from '@/components/ui/logo'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Composition/Footer',
|
||||
component: Footer,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'fullscreen' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['minimal', 'full'],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Footer>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Minimal: Story = {
|
||||
args: {
|
||||
variant: 'minimal',
|
||||
logo: <Logo size="sm" />,
|
||||
copyright: <>© 2026 Greyhaven. All rights reserved.</>,
|
||||
actions: (
|
||||
<div className="flex gap-4 text-sm text-muted-foreground">
|
||||
<a href="#" className="hover:text-foreground transition-colors">Privacy</a>
|
||||
<a href="#" className="hover:text-foreground transition-colors">Terms</a>
|
||||
<a href="#" className="hover:text-foreground transition-colors">Contact</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const Full: Story = {
|
||||
args: {
|
||||
variant: 'full',
|
||||
logo: <Logo size="md" />,
|
||||
copyright: <>© 2026 Greyhaven. All rights reserved.</>,
|
||||
linkGroups: [
|
||||
{
|
||||
title: 'Product',
|
||||
links: [
|
||||
{ label: 'Features', href: '#' },
|
||||
{ label: 'Pricing', href: '#' },
|
||||
{ label: 'Changelog', href: '#' },
|
||||
{ label: 'Docs', href: '#' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ label: 'About', href: '#' },
|
||||
{ label: 'Blog', href: '#' },
|
||||
{ label: 'Careers', href: '#' },
|
||||
{ label: 'Contact', href: '#' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Legal',
|
||||
links: [
|
||||
{ label: 'Privacy Policy', href: '#' },
|
||||
{ label: 'Terms of Service', href: '#' },
|
||||
{ label: 'Cookie Policy', href: '#' },
|
||||
],
|
||||
},
|
||||
],
|
||||
actions: (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm">Twitter</Button>
|
||||
<Button variant="ghost" size="sm">GitHub</Button>
|
||||
<Button variant="ghost" size="sm">Discord</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const MinimalNoCopyright: Story = {
|
||||
args: {
|
||||
variant: 'minimal',
|
||||
logo: <Logo size="sm" />,
|
||||
actions: (
|
||||
<div className="flex gap-4 text-sm text-muted-foreground">
|
||||
<a href="#" className="hover:text-foreground transition-colors">Docs</a>
|
||||
<a href="#" className="hover:text-foreground transition-colors">GitHub</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}
|
||||
111
stories/Composition/Hero.stories.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Hero } from '@/components/ui/hero'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Composition/Hero',
|
||||
component: Hero,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'fullscreen' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['centered', 'left-aligned', 'split'],
|
||||
},
|
||||
background: {
|
||||
control: 'select',
|
||||
options: ['default', 'muted', 'accent', 'dark'],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Hero>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const defaultActions = (
|
||||
<>
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="outline">Learn More</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
export const Centered: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
heading: 'Build better products with Greyhaven',
|
||||
subheading:
|
||||
'A modern design system that helps you create consistent, accessible, and beautiful user interfaces.',
|
||||
actions: defaultActions,
|
||||
},
|
||||
}
|
||||
|
||||
export const LeftAligned: Story = {
|
||||
args: {
|
||||
variant: 'left-aligned',
|
||||
heading: 'Ship faster with confidence',
|
||||
subheading:
|
||||
'Pre-built components, design tokens, and patterns so your team can focus on what matters.',
|
||||
actions: defaultActions,
|
||||
},
|
||||
}
|
||||
|
||||
export const Split: Story = {
|
||||
args: {
|
||||
variant: 'split',
|
||||
heading: 'Design meets engineering',
|
||||
subheading:
|
||||
'Bridging the gap between design and code with a shared language of components and tokens.',
|
||||
actions: defaultActions,
|
||||
media: (
|
||||
<div className="w-full aspect-video bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
|
||||
Image / Media Placeholder
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const MutedBackground: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
background: 'muted',
|
||||
heading: 'Welcome to the platform',
|
||||
subheading: 'Everything you need to build and scale your project.',
|
||||
actions: defaultActions,
|
||||
},
|
||||
}
|
||||
|
||||
export const AccentBackground: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
background: 'accent',
|
||||
heading: 'Start building today',
|
||||
subheading: 'Join thousands of developers using our design system.',
|
||||
actions: defaultActions,
|
||||
},
|
||||
}
|
||||
|
||||
export const DarkBackground: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
background: 'dark',
|
||||
heading: 'The future of design systems',
|
||||
subheading: 'A bold new approach to building consistent user interfaces at scale.',
|
||||
actions: (
|
||||
<>
|
||||
<Button size="lg" variant="secondary">Get Started</Button>
|
||||
<Button size="lg" variant="outline" className="border-background/20 text-background hover:bg-background/10">
|
||||
Learn More
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithoutActions: Story = {
|
||||
args: {
|
||||
variant: 'centered',
|
||||
heading: 'A hero section without action buttons',
|
||||
subheading: 'Sometimes you just need a heading and description.',
|
||||
},
|
||||
}
|
||||
167
stories/Composition/PageLayout.stories.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { PageLayout } from '@/components/ui/page-layout'
|
||||
import { Navbar, NavbarLink } from '@/components/ui/navbar'
|
||||
import { Footer } from '@/components/ui/footer'
|
||||
import { Hero } from '@/components/ui/hero'
|
||||
import { Section } from '@/components/ui/section'
|
||||
import { CTASection } from '@/components/ui/cta-section'
|
||||
import { Logo } from '@/components/ui/logo'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from '@/components/ui/card'
|
||||
|
||||
const meta = {
|
||||
title: 'Composition/PageLayout',
|
||||
component: PageLayout,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'fullscreen' },
|
||||
} satisfies Meta<typeof PageLayout>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const navLinks = (
|
||||
<>
|
||||
<NavbarLink href="#" active>Home</NavbarLink>
|
||||
<NavbarLink href="#">Features</NavbarLink>
|
||||
<NavbarLink href="#">Pricing</NavbarLink>
|
||||
<NavbarLink href="#">Docs</NavbarLink>
|
||||
</>
|
||||
)
|
||||
|
||||
const navActions = (
|
||||
<>
|
||||
<Button variant="ghost" size="sm">Log in</Button>
|
||||
<Button size="sm">Sign up</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
const sampleNavbar = (
|
||||
<Navbar
|
||||
variant="solid"
|
||||
logo={<Logo size="sm" />}
|
||||
actions={navActions}
|
||||
>
|
||||
{navLinks}
|
||||
</Navbar>
|
||||
)
|
||||
|
||||
const sampleFooter = (
|
||||
<Footer
|
||||
variant="minimal"
|
||||
logo={<Logo size="sm" />}
|
||||
copyright={<>© 2026 Greyhaven. All rights reserved.</>}
|
||||
actions={
|
||||
<div className="flex gap-4 text-sm text-muted-foreground">
|
||||
<a href="#" className="hover:text-foreground transition-colors">Privacy</a>
|
||||
<a href="#" className="hover:text-foreground transition-colors">Terms</a>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
export const FullPage: Story = {
|
||||
args: {
|
||||
navbar: sampleNavbar,
|
||||
footer: sampleFooter,
|
||||
children: (
|
||||
<>
|
||||
<Hero
|
||||
variant="centered"
|
||||
heading="Build something great"
|
||||
subheading="A complete design system for modern web applications."
|
||||
actions={
|
||||
<>
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="outline">View Docs</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Section
|
||||
title="Features"
|
||||
description="Everything you need to build beautiful interfaces."
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{['Components', 'Tokens', 'Patterns'].map((title) => (
|
||||
<Card key={title}>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>
|
||||
Pre-built {title.toLowerCase()} for rapid development.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fully customizable {title.toLowerCase()} that follow best practices.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<CTASection
|
||||
background="muted"
|
||||
heading="Ready to start?"
|
||||
description="Get up and running in minutes."
|
||||
actions={<Button size="lg">Get Started Free</Button>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithSidebar: Story = {
|
||||
args: {
|
||||
navbar: sampleNavbar,
|
||||
footer: sampleFooter,
|
||||
sidebar: (
|
||||
<nav className="p-4 space-y-2">
|
||||
<h3 className="font-semibold text-sm mb-4">Navigation</h3>
|
||||
{['Dashboard', 'Projects', 'Team', 'Settings'].map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
href="#"
|
||||
className="block px-3 py-2 text-sm rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
),
|
||||
children: (
|
||||
<Section title="Dashboard" description="Overview of your workspace.">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{['Revenue', 'Users', 'Orders', 'Growth'].map((metric) => (
|
||||
<Card key={metric}>
|
||||
<CardHeader>
|
||||
<CardTitle>{metric}</CardTitle>
|
||||
<CardDescription>Last 30 days</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">1,234</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const ContentOnly: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<Section title="Standalone Content" description="A page layout with no navbar or footer.">
|
||||
<p className="text-muted-foreground">
|
||||
This demonstrates the PageLayout component with only content, no navbar, sidebar, or footer.
|
||||
</p>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
}
|
||||
135
stories/Composition/Section.stories.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Section } from '@/components/ui/section'
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from '@/components/ui/card'
|
||||
|
||||
const meta = {
|
||||
title: 'Composition/Section',
|
||||
component: Section,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'fullscreen' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['default', 'highlighted', 'accent'],
|
||||
},
|
||||
width: {
|
||||
control: 'select',
|
||||
options: ['narrow', 'default', 'wide', 'full'],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Section>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const sampleCards = (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{['Design', 'Develop', 'Deploy'].map((title) => (
|
||||
<Card key={title}>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>Description for the {title.toLowerCase()} phase.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Content explaining the {title.toLowerCase()} process in detail.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Our Process',
|
||||
description: 'How we build great products from concept to delivery.',
|
||||
children: sampleCards,
|
||||
},
|
||||
}
|
||||
|
||||
export const Highlighted: Story = {
|
||||
args: {
|
||||
variant: 'highlighted',
|
||||
title: 'Featured Section',
|
||||
description: 'This section uses a highlighted background to stand out.',
|
||||
children: sampleCards,
|
||||
},
|
||||
}
|
||||
|
||||
export const Accent: Story = {
|
||||
args: {
|
||||
variant: 'accent',
|
||||
title: 'Accent Section',
|
||||
description: 'A subtle accent background to differentiate this area.',
|
||||
children: sampleCards,
|
||||
},
|
||||
}
|
||||
|
||||
export const Narrow: Story = {
|
||||
args: {
|
||||
width: 'narrow',
|
||||
title: 'Narrow Section',
|
||||
description: 'Constrained width for focused reading.',
|
||||
children: (
|
||||
<p className="text-muted-foreground">
|
||||
This is a narrow section with max-w-3xl. Useful for text-heavy content that
|
||||
benefits from shorter line lengths for readability.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const Wide: Story = {
|
||||
args: {
|
||||
width: 'wide',
|
||||
title: 'Wide Section',
|
||||
description: 'Extended width for content-rich layouts.',
|
||||
children: sampleCards,
|
||||
},
|
||||
}
|
||||
|
||||
export const Full: Story = {
|
||||
args: {
|
||||
width: 'full',
|
||||
variant: 'highlighted',
|
||||
title: 'Full Width Section',
|
||||
description: 'Spans the full width of the viewport.',
|
||||
children: sampleCards,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoHeader: Story = {
|
||||
args: {
|
||||
children: sampleCards,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllCombinations: Story = {
|
||||
render: () => (
|
||||
<div>
|
||||
{(['default', 'highlighted', 'accent'] as const).map((variant) =>
|
||||
(['narrow', 'default', 'wide'] as const).map((width) => (
|
||||
<Section
|
||||
key={`${variant}-${width}`}
|
||||
variant={variant}
|
||||
width={width}
|
||||
title={`${variant} / ${width}`}
|
||||
description={`Section with variant="${variant}" and width="${width}".`}
|
||||
>
|
||||
<div className="h-20 rounded-lg border-2 border-dashed border-muted-foreground/25 flex items-center justify-center text-sm text-muted-foreground">
|
||||
Content area
|
||||
</div>
|
||||
</Section>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
88
stories/Data/Progress.stories.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
|
||||
const meta = {
|
||||
title: 'Data/Progress',
|
||||
component: Progress,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
value: {
|
||||
control: { type: 'range', min: 0, max: 100, step: 1 },
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-100">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof Progress>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: 60,
|
||||
},
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
value: 0,
|
||||
},
|
||||
}
|
||||
|
||||
export const Quarter: Story = {
|
||||
args: {
|
||||
value: 25,
|
||||
},
|
||||
}
|
||||
|
||||
export const Half: Story = {
|
||||
args: {
|
||||
value: 50,
|
||||
},
|
||||
}
|
||||
|
||||
export const ThreeQuarters: Story = {
|
||||
args: {
|
||||
value: 75,
|
||||
},
|
||||
}
|
||||
|
||||
export const Complete: Story = {
|
||||
args: {
|
||||
value: 100,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStages: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4 w-100">
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm text-muted-foreground">0%</span>
|
||||
<Progress value={0} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm text-muted-foreground">25%</span>
|
||||
<Progress value={25} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm text-muted-foreground">50%</span>
|
||||
<Progress value={50} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm text-muted-foreground">75%</span>
|
||||
<Progress value={75} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm text-muted-foreground">100%</span>
|
||||
<Progress value={100} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
117
stories/Data/Table.stories.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
} from '@/components/ui/table'
|
||||
|
||||
const meta = {
|
||||
title: 'Data/Table',
|
||||
component: Table,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Table>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const invoices = [
|
||||
{ invoice: 'INV001', status: 'Paid', method: 'Credit Card', amount: '$250.00' },
|
||||
{ invoice: 'INV002', status: 'Pending', method: 'PayPal', amount: '$150.00' },
|
||||
{ invoice: 'INV003', status: 'Unpaid', method: 'Bank Transfer', amount: '$350.00' },
|
||||
{ invoice: 'INV004', status: 'Paid', method: 'Credit Card', amount: '$450.00' },
|
||||
{ invoice: 'INV005', status: 'Paid', method: 'PayPal', amount: '$550.00' },
|
||||
]
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className="w-150">
|
||||
<Table>
|
||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-25">Invoice</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{invoices.map((invoice) => (
|
||||
<TableRow key={invoice.invoice}>
|
||||
<TableCell className="font-medium">{invoice.invoice}</TableCell>
|
||||
<TableCell>{invoice.status}</TableCell>
|
||||
<TableCell>{invoice.method}</TableCell>
|
||||
<TableCell className="text-right">{invoice.amount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3}>Total</TableCell>
|
||||
<TableCell className="text-right">$1,750.00</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Simple: Story = {
|
||||
render: () => (
|
||||
<div className="w-100">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Alice</TableCell>
|
||||
<TableCell>Engineer</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Bob</TableCell>
|
||||
<TableCell>Designer</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Charlie</TableCell>
|
||||
<TableCell>Manager</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
render: () => (
|
||||
<div className="w-100">
|
||||
<Table>
|
||||
<TableCaption>No data available.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center text-muted-foreground h-24">
|
||||
No results found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
71
stories/Feedback/Alert.stories.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { Terminal, AlertCircle } from 'lucide-react'
|
||||
|
||||
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
|
||||
const meta = {
|
||||
title: 'Feedback/Alert',
|
||||
component: Alert,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['default', 'destructive'],
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-125">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof Alert>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Alert>
|
||||
<Terminal className="size-4" />
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can add components to your app using the CLI.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
),
|
||||
}
|
||||
|
||||
export const Destructive: Story = {
|
||||
render: () => (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your session has expired. Please log in again.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithoutIcon: Story = {
|
||||
render: () => (
|
||||
<Alert>
|
||||
<AlertTitle>Note</AlertTitle>
|
||||
<AlertDescription>
|
||||
This alert has no icon, just a title and description.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
),
|
||||
}
|
||||
|
||||
export const TitleOnly: Story = {
|
||||
render: () => (
|
||||
<Alert>
|
||||
<Terminal className="size-4" />
|
||||
<AlertTitle>A simple alert with only a title.</AlertTitle>
|
||||
</Alert>
|
||||
),
|
||||
}
|
||||
63
stories/Feedback/Skeleton.stories.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
const meta = {
|
||||
title: 'Feedback/Skeleton',
|
||||
component: Skeleton,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Skeleton>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
className: 'h-4 w-62.5',
|
||||
},
|
||||
}
|
||||
|
||||
export const Circle: Story = {
|
||||
args: {
|
||||
className: 'size-12 rounded-full',
|
||||
},
|
||||
}
|
||||
|
||||
export const CardSkeleton: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center space-x-4">
|
||||
<Skeleton className="size-12 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-62.5" />
|
||||
<Skeleton className="h-4 w-50" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const FormSkeleton: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4 w-75">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-9 w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-30" />
|
||||
<Skeleton className="h-9 w-full" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-25" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const TextBlock: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-2 w-87.5">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
54
stories/Feedback/Spinner.stories.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
|
||||
const meta = {
|
||||
title: 'Feedback/Spinner',
|
||||
component: Spinner,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Spinner>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
className: 'size-3 animate-spin',
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
className: 'size-8 animate-spin',
|
||||
},
|
||||
}
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
className: 'size-12 animate-spin',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithText: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Spinner />
|
||||
<span className="text-sm text-muted-foreground">Loading...</span>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-4">
|
||||
<Spinner className="size-3 animate-spin" />
|
||||
<Spinner />
|
||||
<Spinner className="size-6 animate-spin" />
|
||||
<Spinner className="size-8 animate-spin" />
|
||||
<Spinner className="size-12 animate-spin" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
211
stories/Form/Form.stories.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import React from 'react'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Form/Form',
|
||||
component: Form,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Form>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const profileSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(2, { message: 'Username must be at least 2 characters.' })
|
||||
.max(30, { message: 'Username must not be longer than 30 characters.' }),
|
||||
email: z.string().email({ message: 'Please enter a valid email address.' }),
|
||||
})
|
||||
|
||||
type ProfileValues = z.infer<typeof profileSchema>
|
||||
|
||||
function ProfileForm() {
|
||||
const form = useForm<ProfileValues>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
email: '',
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: ProfileValues) {
|
||||
alert(JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-100 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="johndoe" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
We will never share your email with anyone.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <ProfileForm />,
|
||||
}
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email({ message: 'Invalid email address.' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, { message: 'Password must be at least 8 characters.' }),
|
||||
})
|
||||
|
||||
type LoginValues = z.infer<typeof loginSchema>
|
||||
|
||||
function LoginForm() {
|
||||
const form = useForm<LoginValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: LoginValues) {
|
||||
alert(JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-100 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="you@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="Enter password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full">
|
||||
Sign in
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const Login: Story = {
|
||||
render: () => <LoginForm />,
|
||||
}
|
||||
|
||||
function PrefilledErrorForm() {
|
||||
const form = useForm<ProfileValues>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
defaultValues: {
|
||||
username: 'a',
|
||||
email: 'not-an-email',
|
||||
},
|
||||
})
|
||||
|
||||
// Trigger validation on mount
|
||||
React.useEffect(() => {
|
||||
form.trigger()
|
||||
}, [form])
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="w-100 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithErrors: Story = {
|
||||
render: () => <PrefilledErrorForm />,
|
||||
}
|
||||
87
stories/Layout/Accordion.stories.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '@/components/ui/accordion'
|
||||
|
||||
const meta = {
|
||||
title: 'Layout/Accordion',
|
||||
component: Accordion,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Accordion>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Single: Story = {
|
||||
render: () => (
|
||||
<Accordion type="single" collapsible className="w-100">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It adheres to the WAI-ARIA design pattern.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Is it styled?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It comes with default styles that match the other components.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Is it animated?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It uses CSS animations for smooth open and close transitions.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
),
|
||||
}
|
||||
|
||||
export const Multiple: Story = {
|
||||
render: () => (
|
||||
<Accordion type="multiple" className="w-100">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>What is Greyhaven?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Greyhaven is a design system built with Radix UI and Tailwind CSS.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>How do I install it?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
You can install it via npm or pnpm. Check the documentation for details.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Can I customize themes?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Absolutely. The design system uses CSS custom properties for full theme control.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
),
|
||||
}
|
||||
|
||||
export const DefaultOpen: Story = {
|
||||
render: () => (
|
||||
<Accordion type="single" defaultValue="item-1" collapsible className="w-100">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Open by default</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
This accordion item is open by default.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Click to open</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
This accordion item starts closed.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
),
|
||||
}
|
||||
97
stories/Layout/Card.stories.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardAction,
|
||||
} from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Layout/Card',
|
||||
component: Card,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Card>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Card className="w-87.5">
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>Card description goes here.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content with some example text to demonstrate the layout.</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button>Action</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithAction: Story = {
|
||||
render: () => (
|
||||
<Card className="w-87.5">
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
<CardDescription>You have 3 unread messages.</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">Mark all read</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Here are your latest notifications.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
),
|
||||
}
|
||||
|
||||
export const Simple: Story = {
|
||||
render: () => (
|
||||
<Card className="w-87.5">
|
||||
<CardHeader>
|
||||
<CardTitle>Simple Card</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>A card with just a title and content.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithFooter: Story = {
|
||||
render: () => (
|
||||
<Card className="w-87.5">
|
||||
<CardHeader>
|
||||
<CardTitle>Create project</CardTitle>
|
||||
<CardDescription>Deploy your new project in one click.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Configure your project settings below.</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Deploy</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
}
|
||||
|
||||
export const ContentOnly: Story = {
|
||||
render: () => (
|
||||
<Card className="w-87.5">
|
||||
<CardContent>
|
||||
<p>A minimal card with only content, no header or footer.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
),
|
||||
}
|
||||
59
stories/Layout/Separator.stories.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
const meta = {
|
||||
title: 'Layout/Separator',
|
||||
component: Separator,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
orientation: {
|
||||
control: 'select',
|
||||
options: ['horizontal', 'vertical'],
|
||||
},
|
||||
decorative: { control: 'boolean' },
|
||||
},
|
||||
} satisfies Meta<typeof Separator>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Horizontal: Story = {
|
||||
args: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-75">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-medium leading-none">Greyhaven Design System</h4>
|
||||
<p className="text-sm text-muted-foreground">An open-source UI component library.</p>
|
||||
</div>
|
||||
<Story />
|
||||
<div className="flex h-5 items-center space-x-4 text-sm">
|
||||
<div>Blog</div>
|
||||
<div>Docs</div>
|
||||
<div>Source</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Vertical: Story = {
|
||||
args: {
|
||||
orientation: 'vertical',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex h-5 items-center space-x-4 text-sm">
|
||||
<div>Blog</div>
|
||||
<Story />
|
||||
<div>Docs</div>
|
||||
<Story />
|
||||
<div>Source</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
109
stories/Navigation/Breadcrumb.stories.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
} from '@/components/ui/breadcrumb'
|
||||
|
||||
const meta = {
|
||||
title: 'Navigation/Breadcrumb',
|
||||
component: Breadcrumb,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Breadcrumb>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Components</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithEllipsis: Story = {
|
||||
render: () => (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbEllipsis />
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Components</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
),
|
||||
}
|
||||
|
||||
export const TwoLevels: Story = {
|
||||
render: () => (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Dashboard</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Settings</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
),
|
||||
}
|
||||
|
||||
export const DeepNesting: Story = {
|
||||
render: () => (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Products</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Electronics</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Laptops</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>MacBook Pro</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
),
|
||||
}
|
||||
94
stories/Navigation/Navbar.stories.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Navbar, NavbarLink } from '@/components/ui/navbar'
|
||||
import { Logo } from '@/components/ui/logo'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Navigation/Navbar',
|
||||
component: Navbar,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'fullscreen' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['solid', 'transparent', 'minimal'],
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="min-h-[200px]">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof Navbar>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const navLinks = (
|
||||
<>
|
||||
<NavbarLink href="#" active>Home</NavbarLink>
|
||||
<NavbarLink href="#">About</NavbarLink>
|
||||
<NavbarLink href="#">Services</NavbarLink>
|
||||
<NavbarLink href="#">Contact</NavbarLink>
|
||||
</>
|
||||
)
|
||||
|
||||
const navActions = (
|
||||
<>
|
||||
<Button variant="ghost" size="sm">Log in</Button>
|
||||
<Button size="sm">Sign up</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
export const Solid: Story = {
|
||||
args: {
|
||||
variant: 'solid',
|
||||
logo: <Logo size="sm" />,
|
||||
actions: navActions,
|
||||
children: navLinks,
|
||||
},
|
||||
}
|
||||
|
||||
export const Transparent: Story = {
|
||||
args: {
|
||||
variant: 'transparent',
|
||||
logo: <Logo size="sm" />,
|
||||
actions: navActions,
|
||||
children: navLinks,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="min-h-[200px] bg-gradient-to-br from-primary/20 to-primary/5">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Minimal: Story = {
|
||||
args: {
|
||||
variant: 'minimal',
|
||||
logo: <Logo size="sm" />,
|
||||
actions: navActions,
|
||||
children: navLinks,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithoutActions: Story = {
|
||||
args: {
|
||||
variant: 'solid',
|
||||
logo: <Logo size="sm" />,
|
||||
children: navLinks,
|
||||
},
|
||||
}
|
||||
|
||||
export const LogoOnly: Story = {
|
||||
args: {
|
||||
variant: 'solid',
|
||||
logo: <Logo size="sm" />,
|
||||
actions: navActions,
|
||||
},
|
||||
}
|
||||
72
stories/Overlay/AlertDialog.stories.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Overlay/AlertDialog',
|
||||
component: AlertDialog,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof AlertDialog>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Delete Account</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your account
|
||||
and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Continue</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
}
|
||||
|
||||
export const Destructive: Story = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">Delete Project</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete project?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete the project and all associated data.
|
||||
This action cannot be reversed.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction className="bg-destructive text-white hover:bg-destructive/90">
|
||||
Yes, delete project
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
}
|
||||
106
stories/Overlay/Dialog.stories.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
const meta = {
|
||||
title: 'Overlay/Dialog',
|
||||
component: Dialog,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Dialog>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>
|
||||
This is a dialog description. It provides context about the dialog content.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p>Dialog body content goes here.</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Confirm</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithForm: Story = {
|
||||
render: () => (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Edit Profile</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit profile</DialogTitle>
|
||||
<DialogDescription>
|
||||
Make changes to your profile here. Click save when you are done.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" defaultValue="John Doe" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input id="username" defaultValue="@johndoe" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit">Save changes</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
}
|
||||
|
||||
export const NoCloseButton: Story = {
|
||||
render: () => (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Open (no close button)</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>No Close Button</DialogTitle>
|
||||
<DialogDescription>
|
||||
This dialog has no close button in the corner.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button>Got it</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
}
|
||||
99
stories/Overlay/Tooltip.stories.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { Plus } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Overlay/Tooltip',
|
||||
component: Tooltip,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies Meta<typeof Tooltip>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">Hover me</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This is a tooltip</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
export const Top: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">Top</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Tooltip on top</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
export const Right: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">Right</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Tooltip on right</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
export const Bottom: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">Bottom</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Tooltip on bottom</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
export const Left: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">Left</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Tooltip on left</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithIconButton: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="outline">
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Add item</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
223
stories/Primitives/Badge.stories.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const meta = {
|
||||
title: 'Primitives/Badge',
|
||||
component: Badge,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'default',
|
||||
'secondary',
|
||||
'muted',
|
||||
'destructive',
|
||||
'outline',
|
||||
'success',
|
||||
'warning',
|
||||
'info',
|
||||
'tag',
|
||||
'value',
|
||||
'whatsapp',
|
||||
'email',
|
||||
'telegram',
|
||||
'zulip',
|
||||
'platform',
|
||||
],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'default', 'lg'],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Badge>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Badge',
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: 'Secondary',
|
||||
variant: 'secondary',
|
||||
},
|
||||
}
|
||||
|
||||
export const Muted: Story = {
|
||||
args: {
|
||||
children: 'Muted',
|
||||
variant: 'muted',
|
||||
},
|
||||
}
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
children: 'Destructive',
|
||||
variant: 'destructive',
|
||||
},
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
children: 'Outline',
|
||||
variant: 'outline',
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
children: 'Success',
|
||||
variant: 'success',
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
children: 'Warning',
|
||||
variant: 'warning',
|
||||
},
|
||||
}
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
children: 'Info',
|
||||
variant: 'info',
|
||||
},
|
||||
}
|
||||
|
||||
export const Tag: Story = {
|
||||
args: {
|
||||
children: 'Tag',
|
||||
variant: 'tag',
|
||||
},
|
||||
}
|
||||
|
||||
export const Value: Story = {
|
||||
args: {
|
||||
children: '42',
|
||||
variant: 'value',
|
||||
},
|
||||
}
|
||||
|
||||
export const Whatsapp: Story = {
|
||||
args: {
|
||||
children: 'WhatsApp',
|
||||
variant: 'whatsapp',
|
||||
},
|
||||
}
|
||||
|
||||
export const Email: Story = {
|
||||
args: {
|
||||
children: 'Email',
|
||||
variant: 'email',
|
||||
},
|
||||
}
|
||||
|
||||
export const Telegram: Story = {
|
||||
args: {
|
||||
children: 'Telegram',
|
||||
variant: 'telegram',
|
||||
},
|
||||
}
|
||||
|
||||
export const Zulip: Story = {
|
||||
args: {
|
||||
children: 'Zulip',
|
||||
variant: 'zulip',
|
||||
},
|
||||
}
|
||||
|
||||
export const Platform: Story = {
|
||||
args: {
|
||||
children: 'Platform',
|
||||
variant: 'platform',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(
|
||||
[
|
||||
'default',
|
||||
'secondary',
|
||||
'muted',
|
||||
'destructive',
|
||||
'outline',
|
||||
'success',
|
||||
'warning',
|
||||
'info',
|
||||
'tag',
|
||||
'value',
|
||||
'whatsapp',
|
||||
'email',
|
||||
'telegram',
|
||||
'zulip',
|
||||
'platform',
|
||||
] as const
|
||||
).map((variant) => (
|
||||
<Badge key={variant} variant={variant}>
|
||||
{variant}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const SizeSmall: Story = {
|
||||
args: {
|
||||
children: 'Small',
|
||||
variant: 'secondary',
|
||||
size: 'sm',
|
||||
},
|
||||
}
|
||||
|
||||
export const SizeDefault: Story = {
|
||||
args: {
|
||||
children: 'Default',
|
||||
variant: 'secondary',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
|
||||
export const SizeLarge: Story = {
|
||||
args: {
|
||||
children: 'Large',
|
||||
variant: 'default',
|
||||
size: 'lg',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-20 text-sm text-muted-foreground font-sans">sm</span>
|
||||
<Badge variant="secondary" size="sm">dense</Badge>
|
||||
<Badge variant="success" size="sm">12 new</Badge>
|
||||
<Badge variant="muted" size="sm">draft</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-20 text-sm text-muted-foreground font-sans">default</span>
|
||||
<Badge variant="default" size="default">Active</Badge>
|
||||
<Badge variant="success" size="default">Published</Badge>
|
||||
<Badge variant="warning" size="default">Pending</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-20 text-sm text-muted-foreground font-sans">lg</span>
|
||||
<Badge variant="default" size="lg">New feature</Badge>
|
||||
<Badge variant="info" size="lg">Beta</Badge>
|
||||
<Badge variant="success" size="lg">Available</Badge>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
171
stories/Primitives/Button.stories.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { ChevronRight, Mail, Loader2, Plus } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const meta = {
|
||||
title: 'Primitives/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['default', 'secondary', 'outline', 'ghost', 'link', 'destructive'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['default', 'sm', 'lg', 'icon', 'icon-sm', 'icon-lg'],
|
||||
},
|
||||
disabled: { control: 'boolean' },
|
||||
asChild: { control: 'boolean' },
|
||||
},
|
||||
} satisfies Meta<typeof Button>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Button',
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: 'Secondary',
|
||||
variant: 'secondary',
|
||||
},
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
children: 'Outline',
|
||||
variant: 'outline',
|
||||
},
|
||||
}
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: {
|
||||
children: 'Ghost',
|
||||
variant: 'ghost',
|
||||
},
|
||||
}
|
||||
|
||||
export const Link: Story = {
|
||||
args: {
|
||||
children: 'Link',
|
||||
variant: 'link',
|
||||
},
|
||||
}
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
children: 'Delete',
|
||||
variant: 'destructive',
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
children: 'Small',
|
||||
size: 'sm',
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
children: 'Large',
|
||||
size: 'lg',
|
||||
},
|
||||
}
|
||||
|
||||
export const Icon: Story = {
|
||||
args: {
|
||||
size: 'icon',
|
||||
children: <Plus className="size-4" />,
|
||||
'aria-label': 'Add',
|
||||
},
|
||||
}
|
||||
|
||||
export const IconSmall: Story = {
|
||||
args: {
|
||||
size: 'icon-sm',
|
||||
children: <Plus className="size-4" />,
|
||||
'aria-label': 'Add',
|
||||
},
|
||||
}
|
||||
|
||||
export const IconLarge: Story = {
|
||||
args: {
|
||||
size: 'icon-lg',
|
||||
children: <Plus className="size-4" />,
|
||||
'aria-label': 'Add',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<Mail /> Login with Email
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithTrailingIcon: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
Next <ChevronRight />
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
children: (
|
||||
<>
|
||||
<Loader2 className="animate-spin" /> Please wait
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: 'Disabled',
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AsChild: Story = {
|
||||
args: {
|
||||
asChild: true,
|
||||
children: <a href="#">Link styled as Button</a>,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
{(['default', 'secondary', 'outline', 'ghost', 'link', 'destructive'] as const).map(
|
||||
(variant) => (
|
||||
<div key={variant} className="flex items-center gap-2">
|
||||
<span className="w-24 text-sm text-muted-foreground">{variant}</span>
|
||||
<Button variant={variant} size="sm">Small</Button>
|
||||
<Button variant={variant} size="default">Default</Button>
|
||||
<Button variant={variant} size="lg">Large</Button>
|
||||
<Button variant={variant} size="icon"><Plus className="size-4" /></Button>
|
||||
<Button variant={variant} disabled>Disabled</Button>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
90
stories/Primitives/Code.stories.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Code } from '@/components/ui/code'
|
||||
|
||||
const meta = {
|
||||
title: 'Primitives/Code',
|
||||
component: Code,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['inline', 'block'],
|
||||
},
|
||||
language: { control: 'text' },
|
||||
},
|
||||
} satisfies Meta<typeof Code>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Inline: Story = {
|
||||
args: {
|
||||
variant: 'inline',
|
||||
children: 'pnpm install',
|
||||
},
|
||||
}
|
||||
|
||||
export const InlineInSentence: Story = {
|
||||
render: () => (
|
||||
<p className="font-serif text-base leading-relaxed max-w-prose">
|
||||
To get started, run <Code>pnpm install</Code> and then{' '}
|
||||
<Code>pnpm dev</Code> to start the development server on{' '}
|
||||
<Code>localhost:3000</Code>.
|
||||
</p>
|
||||
),
|
||||
}
|
||||
|
||||
export const Block: Story = {
|
||||
args: {
|
||||
variant: 'block',
|
||||
language: 'bash',
|
||||
children: `pnpm install
|
||||
pnpm dev
|
||||
pnpm build`,
|
||||
},
|
||||
}
|
||||
|
||||
export const BlockLongCommand: Story = {
|
||||
args: {
|
||||
variant: 'block',
|
||||
language: 'bash',
|
||||
children:
|
||||
'curl -fsSL https://example.com/install.sh | bash -s -- --prefix=/usr/local --no-color --very-long-flag-that-should-wrap',
|
||||
},
|
||||
}
|
||||
|
||||
export const BlockTypescript: Story = {
|
||||
args: {
|
||||
variant: 'block',
|
||||
language: 'ts',
|
||||
children: `import { Code } from '@/components/ui/code'
|
||||
|
||||
export function Example() {
|
||||
return <Code variant="block">Hello, world!</Code>
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-6 max-w-2xl">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-2 font-sans">Inline</p>
|
||||
<p className="font-serif">
|
||||
Use <Code>cn()</Code> from <Code>@/lib/utils</Code> to merge Tailwind classes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-2 font-sans">Block</p>
|
||||
<Code variant="block" language="bash">
|
||||
{`# install and run
|
||||
pnpm install
|
||||
pnpm dev`}
|
||||
</Code>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
88
stories/Primitives/Input.stories.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
const meta = {
|
||||
title: 'Primitives/Input',
|
||||
component: Input,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['text', 'email', 'password', 'number', 'search', 'tel', 'url', 'file'],
|
||||
},
|
||||
disabled: { control: 'boolean' },
|
||||
placeholder: { control: 'text' },
|
||||
},
|
||||
} satisfies Meta<typeof Input>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: 'Enter text...',
|
||||
type: 'text',
|
||||
},
|
||||
}
|
||||
|
||||
export const Email: Story = {
|
||||
args: {
|
||||
type: 'email',
|
||||
placeholder: 'email@example.com',
|
||||
},
|
||||
}
|
||||
|
||||
export const Password: Story = {
|
||||
args: {
|
||||
type: 'password',
|
||||
placeholder: 'Enter password...',
|
||||
},
|
||||
}
|
||||
|
||||
export const File: Story = {
|
||||
args: {
|
||||
type: 'file',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
placeholder: 'Disabled input',
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithLabel: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-full max-w-sm gap-2">
|
||||
<Label htmlFor="with-label-input">Email</Label>
|
||||
<Input id="with-label-input" type="email" placeholder="email@example.com" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const ErrorState: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-full max-w-sm gap-2">
|
||||
<Label htmlFor="error-input">Email</Label>
|
||||
<Input
|
||||
id="error-input"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
aria-invalid="true"
|
||||
defaultValue="invalid-email"
|
||||
/>
|
||||
<p className="text-sm text-destructive">Please enter a valid email address.</p>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithDefaultValue: Story = {
|
||||
args: {
|
||||
type: 'text',
|
||||
defaultValue: 'Hello world',
|
||||
},
|
||||
}
|
||||
103
stories/Primitives/Toggle.stories.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { Bold, Italic, Underline } from 'lucide-react'
|
||||
|
||||
import { Toggle } from '@/components/ui/toggle'
|
||||
|
||||
const meta = {
|
||||
title: 'Primitives/Toggle',
|
||||
component: Toggle,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['default', 'outline'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['default', 'sm', 'lg'],
|
||||
},
|
||||
disabled: { control: 'boolean' },
|
||||
},
|
||||
} satisfies Meta<typeof Toggle>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: <Bold className="size-4" />,
|
||||
'aria-label': 'Toggle bold',
|
||||
},
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
variant: 'outline',
|
||||
children: <Italic className="size-4" />,
|
||||
'aria-label': 'Toggle italic',
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: 'sm',
|
||||
children: <Bold className="size-4" />,
|
||||
'aria-label': 'Toggle bold',
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: 'lg',
|
||||
children: <Bold className="size-4" />,
|
||||
'aria-label': 'Toggle bold',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithText: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<Italic className="size-4" />
|
||||
Italic
|
||||
</>
|
||||
),
|
||||
'aria-label': 'Toggle italic',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
children: <Bold className="size-4" />,
|
||||
'aria-label': 'Toggle bold',
|
||||
},
|
||||
}
|
||||
|
||||
export const Pressed: Story = {
|
||||
args: {
|
||||
defaultPressed: true,
|
||||
children: <Bold className="size-4" />,
|
||||
'aria-label': 'Toggle bold',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-20 text-sm text-muted-foreground">default</span>
|
||||
<Toggle size="sm" aria-label="Bold"><Bold className="size-4" /></Toggle>
|
||||
<Toggle size="default" aria-label="Italic"><Italic className="size-4" /></Toggle>
|
||||
<Toggle size="lg" aria-label="Underline"><Underline className="size-4" /></Toggle>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-20 text-sm text-muted-foreground">outline</span>
|
||||
<Toggle variant="outline" size="sm" aria-label="Bold"><Bold className="size-4" /></Toggle>
|
||||
<Toggle variant="outline" size="default" aria-label="Italic"><Italic className="size-4" /></Toggle>
|
||||
<Toggle variant="outline" size="lg" aria-label="Underline"><Underline className="size-4" /></Toggle>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
190
style-dictionary.config.mjs
Normal file
@@ -0,0 +1,190 @@
|
||||
import StyleDictionary from 'style-dictionary';
|
||||
|
||||
// Custom format: CSS custom properties with RGB triplets for Tailwind v4 compatibility
|
||||
// Outputs `--background: 240 240 236;` format that existing components expect
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'css/rgb-variables',
|
||||
format: ({ dictionary, options }) => {
|
||||
const selector = options.selector || ':root';
|
||||
const header = options.header || '';
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (!result) return null;
|
||||
return `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`;
|
||||
}
|
||||
|
||||
// Map semantic token paths to CSS variable names
|
||||
function getCssVarName(token) {
|
||||
const path = token.path;
|
||||
// color.semantic.X or color.dark.X → --X
|
||||
if (path[0] === 'color' && (path[1] === 'semantic' || path[1] === 'dark')) {
|
||||
const rest = path.slice(2);
|
||||
// Handle nested like chart.1, sidebar.background
|
||||
if (rest[0] === 'chart') return `--chart-${rest[1]}`;
|
||||
if (rest[0] === 'sidebar') {
|
||||
const subParts = rest.slice(1);
|
||||
if (subParts.length === 1 && subParts[0] === 'background') return '--sidebar';
|
||||
return `--sidebar-${subParts.join('-')}`;
|
||||
}
|
||||
return `--${rest.join('-')}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = [];
|
||||
dictionary.allTokens.forEach((token) => {
|
||||
const varName = getCssVarName(token);
|
||||
if (!varName) return;
|
||||
|
||||
const value = token.value || token.$value;
|
||||
const rgb = hexToRgb(value);
|
||||
if (rgb) {
|
||||
const desc = token.$description || token.description;
|
||||
if (desc) lines.push(` /* ${desc} */`);
|
||||
lines.push(` ${varName}: ${rgb};`);
|
||||
}
|
||||
});
|
||||
|
||||
return `${header}\n${selector} {\n${lines.join('\n')}\n}`;
|
||||
},
|
||||
});
|
||||
|
||||
// Custom format: TypeScript constants
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'typescript/constants',
|
||||
format: ({ dictionary }) => {
|
||||
const lines = [
|
||||
'// Auto-generated by Style Dictionary — DO NOT EDIT',
|
||||
'// Source: tokens/*.json (W3C DTCG format)',
|
||||
'',
|
||||
];
|
||||
|
||||
// Group tokens by top-level category
|
||||
const groups = {};
|
||||
dictionary.allTokens.forEach((token) => {
|
||||
const group = token.path[0];
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push(token);
|
||||
});
|
||||
|
||||
for (const [group, tokens] of Object.entries(groups)) {
|
||||
const constName = group.charAt(0).toUpperCase() + group.slice(1) + 'Tokens';
|
||||
lines.push(`export const ${constName} = {`);
|
||||
tokens.forEach((token) => {
|
||||
const key = token.path.slice(1).join('.');
|
||||
const value = token.value || token.$value;
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
lines.push(` '${key}': '${value}',`);
|
||||
}
|
||||
});
|
||||
lines.push('} as const;');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
// Custom format: Markdown reference
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'markdown/reference',
|
||||
format: ({ dictionary }) => {
|
||||
const lines = [
|
||||
'# Greyhaven Design Tokens Reference',
|
||||
'',
|
||||
'> Auto-generated by Style Dictionary — DO NOT EDIT',
|
||||
'> Source: `tokens/*.json` (W3C DTCG format)',
|
||||
'',
|
||||
];
|
||||
|
||||
const groups = {};
|
||||
dictionary.allTokens.forEach((token) => {
|
||||
const group = token.path[0];
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push(token);
|
||||
});
|
||||
|
||||
for (const [group, tokens] of Object.entries(groups)) {
|
||||
lines.push(`## ${group.charAt(0).toUpperCase() + group.slice(1)}`);
|
||||
lines.push('');
|
||||
lines.push('| Token | Value | Description |');
|
||||
lines.push('|-------|-------|-------------|');
|
||||
tokens.forEach((token) => {
|
||||
const name = token.path.join('.');
|
||||
const value = token.value || token.$value;
|
||||
const desc = token.$description || token.description || '';
|
||||
const displayValue = typeof value === 'object' ? JSON.stringify(value) : value;
|
||||
lines.push(`| \`${name}\` | \`${displayValue}\` | ${desc} |`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
source: ['tokens/**/*.json'],
|
||||
preprocessors: ['tokens-studio'],
|
||||
platforms: {
|
||||
// CSS custom properties for light theme (semantic tokens)
|
||||
cssLight: {
|
||||
transformGroup: 'css',
|
||||
buildPath: 'app/tokens/',
|
||||
files: [
|
||||
{
|
||||
destination: 'tokens-light.css',
|
||||
format: 'css/rgb-variables',
|
||||
filter: (token) => {
|
||||
return token.path[0] === 'color' && token.path[1] === 'semantic';
|
||||
},
|
||||
options: {
|
||||
selector: ':root',
|
||||
header: '/* Greyhaven Design Tokens — Light Theme\n Auto-generated by Style Dictionary — DO NOT EDIT\n Source: tokens/color.json */\n',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// CSS custom properties for dark theme
|
||||
cssDark: {
|
||||
transformGroup: 'css',
|
||||
buildPath: 'app/tokens/',
|
||||
files: [
|
||||
{
|
||||
destination: 'tokens-dark.css',
|
||||
format: 'css/rgb-variables',
|
||||
filter: (token) => {
|
||||
return token.path[0] === 'color' && token.path[1] === 'dark';
|
||||
},
|
||||
options: {
|
||||
selector: '.dark',
|
||||
header: '/* Greyhaven Design Tokens — Dark Theme\n Auto-generated by Style Dictionary — DO NOT EDIT\n Source: tokens/color.json */\n',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// TypeScript constants
|
||||
ts: {
|
||||
transformGroup: 'js',
|
||||
buildPath: 'app/tokens/',
|
||||
files: [
|
||||
{
|
||||
destination: 'tokens.ts',
|
||||
format: 'typescript/constants',
|
||||
},
|
||||
],
|
||||
},
|
||||
// Markdown reference
|
||||
docs: {
|
||||
transformGroup: 'css',
|
||||
buildPath: 'app/tokens/',
|
||||
files: [
|
||||
{
|
||||
destination: 'TOKENS.md',
|
||||
format: 'markdown/reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
168
tokens/build/TOKENS.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Greyhaven Design Tokens Reference
|
||||
|
||||
> Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
> Source: `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` | `#f0f0ec` | Page background |
|
||||
| `color.semantic.foreground` | `#161614` | Primary text |
|
||||
| `color.semantic.card` | `#f9f9f7` | Card/elevated surface background |
|
||||
| `color.semantic.card-foreground` | `#161614` | Card text |
|
||||
| `color.semantic.popover` | `#f9f9f7` | Popover background |
|
||||
| `color.semantic.popover-foreground` | `#161614` | Popover text |
|
||||
| `color.semantic.primary` | `#d95e2a` | Primary accent — buttons, links, focus rings |
|
||||
| `color.semantic.primary-foreground` | `#f9f9f7` | Text on primary accent |
|
||||
| `color.semantic.secondary` | `#f0f0ec` | Secondary button/surface |
|
||||
| `color.semantic.secondary-foreground` | `#2f2f2c` | Text on secondary surface |
|
||||
| `color.semantic.muted` | `#f0f0ec` | Muted/subdued background |
|
||||
| `color.semantic.muted-foreground` | `#575753` | Muted/subdued text |
|
||||
| `color.semantic.accent` | `#ddddd7` | Subtle hover state |
|
||||
| `color.semantic.accent-foreground` | `#161614` | Text on accent hover |
|
||||
| `color.semantic.destructive` | `#b43232` | Destructive/error actions |
|
||||
| `color.semantic.destructive-foreground` | `#f9f9f7` | Text on destructive |
|
||||
| `color.semantic.border` | `#c4c4bd` | Default border |
|
||||
| `color.semantic.input` | `#c4c4bd` | Input border |
|
||||
| `color.semantic.ring` | `#d95e2a` | Focus ring |
|
||||
| `color.semantic.chart.1` | `#d95e2a` | Chart accent |
|
||||
| `color.semantic.chart.2` | `#575753` | Chart secondary |
|
||||
| `color.semantic.chart.3` | `#7f7f79` | Chart tertiary |
|
||||
| `color.semantic.chart.4` | `#a6a69f` | Chart quaternary |
|
||||
| `color.semantic.chart.5` | `#2f2f2c` | Chart quinary |
|
||||
| `color.semantic.sidebar.background` | `#f0f0ec` | Sidebar background |
|
||||
| `color.semantic.sidebar.foreground` | `#161614` | Sidebar text |
|
||||
| `color.semantic.sidebar.primary` | `#d95e2a` | Sidebar primary accent |
|
||||
| `color.semantic.sidebar.primary-foreground` | `#f9f9f7` | Sidebar primary text |
|
||||
| `color.semantic.sidebar.accent` | `#c4c4bd` | Sidebar accent/hover |
|
||||
| `color.semantic.sidebar.accent-foreground` | `#161614` | Sidebar accent text |
|
||||
| `color.semantic.sidebar.border` | `#c4c4bd` | Sidebar border |
|
||||
| `color.semantic.sidebar.ring` | `#d95e2a` | Sidebar focus ring |
|
||||
| `color.dark.background` | `#161614` | Dark page background |
|
||||
| `color.dark.foreground` | `#f9f9f7` | Dark primary text |
|
||||
| `color.dark.card` | `#2f2f2c` | Dark card surface |
|
||||
| `color.dark.card-foreground` | `#f9f9f7` | Dark card text |
|
||||
| `color.dark.popover` | `#2f2f2c` | Dark popover |
|
||||
| `color.dark.popover-foreground` | `#f9f9f7` | Dark popover text |
|
||||
| `color.dark.primary` | `#d95e2a` | Same orange in dark mode |
|
||||
| `color.dark.primary-foreground` | `#f9f9f7` | Dark primary foreground |
|
||||
| `color.dark.secondary` | `#575753` | Dark secondary |
|
||||
| `color.dark.secondary-foreground` | `#f9f9f7` | Dark secondary text |
|
||||
| `color.dark.muted` | `#575753` | Dark muted |
|
||||
| `color.dark.muted-foreground` | `#c4c4bd` | Dark muted text |
|
||||
| `color.dark.accent` | `#575753` | Dark accent/hover |
|
||||
| `color.dark.accent-foreground` | `#f9f9f7` | Dark accent text |
|
||||
| `color.dark.destructive` | `#b43232` | Same destructive in dark mode |
|
||||
| `color.dark.destructive-foreground` | `#f9f9f7` | Dark destructive text |
|
||||
| `color.dark.border` | `#575753` | Dark border |
|
||||
| `color.dark.input` | `#575753` | Dark input border |
|
||||
| `color.dark.ring` | `#d95e2a` | Dark focus ring |
|
||||
| `color.dark.chart.1` | `#d95e2a` | Dark chart accent |
|
||||
| `color.dark.chart.2` | `#c4c4bd` | Dark chart secondary |
|
||||
| `color.dark.chart.3` | `#a6a69f` | Dark chart tertiary |
|
||||
| `color.dark.chart.4` | `#7f7f79` | Dark chart quaternary |
|
||||
| `color.dark.chart.5` | `#f0f0ec` | Dark chart quinary |
|
||||
| `color.dark.sidebar.background` | `#2f2f2c` | Dark sidebar background |
|
||||
| `color.dark.sidebar.foreground` | `#f9f9f7` | Dark sidebar text |
|
||||
| `color.dark.sidebar.primary` | `#d95e2a` | Dark sidebar primary |
|
||||
| `color.dark.sidebar.primary-foreground` | `#f9f9f7` | Dark sidebar primary text |
|
||||
| `color.dark.sidebar.accent` | `#575753` | Dark sidebar accent |
|
||||
| `color.dark.sidebar.accent-foreground` | `#f9f9f7` | Dark sidebar accent text |
|
||||
| `color.dark.sidebar.border` | `#575753` | Dark sidebar border |
|
||||
| `color.dark.sidebar.ring` | `#d95e2a` | Dark sidebar ring |
|
||||
|
||||
## 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` | `cubic-bezier(0.4, 0, 0.2, 1)` | Standard ease-in-out |
|
||||
| `motion.easing.in` | `cubic-bezier(0.4, 0, 1, 1)` | Ease-in for exits |
|
||||
| `motion.easing.out` | `cubic-bezier(0, 0, 0.2, 1)` | Ease-out for entrances |
|
||||
|
||||
## 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) |
|
||||
|
||||
## Shadow
|
||||
|
||||
| Token | Value | Description |
|
||||
|-------|-------|-------------|
|
||||
| `shadow.xs` | `0 1px 2px 0 rgba(22, 22, 20, 0.05)` | Subtle shadow for buttons, inputs |
|
||||
| `shadow.sm` | `0 1px 3px 0 rgba(22, 22, 20, 0.1)` | Small shadow for cards |
|
||||
| `shadow.md` | `0 4px 6px -1px rgba(22, 22, 20, 0.1)` | Medium shadow for dropdowns, popovers |
|
||||
| `shadow.lg` | `0 10px 15px -3px rgba(22, 22, 20, 0.1)` | Large shadow for dialogs, modals |
|
||||
|
||||
## 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) |
|
||||
|
||||
## Typography
|
||||
|
||||
| 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.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.25rem` | Headings |
|
||||
| `typography.lineHeight.normal` | `1.5rem` | Default |
|
||||
| `typography.lineHeight.relaxed` | `1.625rem` | 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 |
|
||||
70
tokens/build/tokens-dark.css
Normal file
@@ -0,0 +1,70 @@
|
||||
/* Greyhaven Design Tokens — Dark Theme
|
||||
Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
Source: tokens/color.json */
|
||||
|
||||
.dark {
|
||||
/* Dark page background */
|
||||
--background: 22 22 20;
|
||||
/* Dark primary text */
|
||||
--foreground: 249 249 247;
|
||||
/* Dark card surface */
|
||||
--card: 47 47 44;
|
||||
/* Dark card text */
|
||||
--card-foreground: 249 249 247;
|
||||
/* Dark popover */
|
||||
--popover: 47 47 44;
|
||||
/* Dark popover text */
|
||||
--popover-foreground: 249 249 247;
|
||||
/* Same orange in dark mode */
|
||||
--primary: 217 94 42;
|
||||
/* Dark primary foreground */
|
||||
--primary-foreground: 249 249 247;
|
||||
/* Dark secondary */
|
||||
--secondary: 87 87 83;
|
||||
/* Dark secondary text */
|
||||
--secondary-foreground: 249 249 247;
|
||||
/* Dark muted */
|
||||
--muted: 87 87 83;
|
||||
/* Dark muted text */
|
||||
--muted-foreground: 196 196 189;
|
||||
/* Dark accent/hover */
|
||||
--accent: 87 87 83;
|
||||
/* Dark accent text */
|
||||
--accent-foreground: 249 249 247;
|
||||
/* Same destructive in dark mode */
|
||||
--destructive: 180 50 50;
|
||||
/* Dark destructive text */
|
||||
--destructive-foreground: 249 249 247;
|
||||
/* Dark border */
|
||||
--border: 87 87 83;
|
||||
/* Dark input border */
|
||||
--input: 87 87 83;
|
||||
/* Dark focus ring */
|
||||
--ring: 217 94 42;
|
||||
/* Dark chart accent */
|
||||
--chart-1: 217 94 42;
|
||||
/* Dark chart secondary */
|
||||
--chart-2: 196 196 189;
|
||||
/* Dark chart tertiary */
|
||||
--chart-3: 166 166 159;
|
||||
/* Dark chart quaternary */
|
||||
--chart-4: 127 127 121;
|
||||
/* Dark chart quinary */
|
||||
--chart-5: 240 240 236;
|
||||
/* Dark sidebar background */
|
||||
--sidebar: 47 47 44;
|
||||
/* Dark sidebar text */
|
||||
--sidebar-foreground: 249 249 247;
|
||||
/* Dark sidebar primary */
|
||||
--sidebar-primary: 217 94 42;
|
||||
/* Dark sidebar primary text */
|
||||
--sidebar-primary-foreground: 249 249 247;
|
||||
/* Dark sidebar accent */
|
||||
--sidebar-accent: 87 87 83;
|
||||
/* Dark sidebar accent text */
|
||||
--sidebar-accent-foreground: 249 249 247;
|
||||
/* Dark sidebar border */
|
||||
--sidebar-border: 87 87 83;
|
||||
/* Dark sidebar ring */
|
||||
--sidebar-ring: 217 94 42;
|
||||
}
|
||||
70
tokens/build/tokens-light.css
Normal file
@@ -0,0 +1,70 @@
|
||||
/* Greyhaven Design Tokens — Light Theme
|
||||
Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
Source: tokens/color.json */
|
||||
|
||||
:root {
|
||||
/* Page background */
|
||||
--background: 240 240 236;
|
||||
/* Primary text */
|
||||
--foreground: 22 22 20;
|
||||
/* Card/elevated surface background */
|
||||
--card: 249 249 247;
|
||||
/* Card text */
|
||||
--card-foreground: 22 22 20;
|
||||
/* Popover background */
|
||||
--popover: 249 249 247;
|
||||
/* Popover text */
|
||||
--popover-foreground: 22 22 20;
|
||||
/* Primary accent — buttons, links, focus rings */
|
||||
--primary: 217 94 42;
|
||||
/* Text on primary accent */
|
||||
--primary-foreground: 249 249 247;
|
||||
/* Secondary button/surface */
|
||||
--secondary: 240 240 236;
|
||||
/* Text on secondary surface */
|
||||
--secondary-foreground: 47 47 44;
|
||||
/* Muted/subdued background */
|
||||
--muted: 240 240 236;
|
||||
/* Muted/subdued text */
|
||||
--muted-foreground: 87 87 83;
|
||||
/* Subtle hover state */
|
||||
--accent: 221 221 215;
|
||||
/* Text on accent hover */
|
||||
--accent-foreground: 22 22 20;
|
||||
/* Destructive/error actions */
|
||||
--destructive: 180 50 50;
|
||||
/* Text on destructive */
|
||||
--destructive-foreground: 249 249 247;
|
||||
/* Default border */
|
||||
--border: 196 196 189;
|
||||
/* Input border */
|
||||
--input: 196 196 189;
|
||||
/* Focus ring */
|
||||
--ring: 217 94 42;
|
||||
/* Chart accent */
|
||||
--chart-1: 217 94 42;
|
||||
/* Chart secondary */
|
||||
--chart-2: 87 87 83;
|
||||
/* Chart tertiary */
|
||||
--chart-3: 127 127 121;
|
||||
/* Chart quaternary */
|
||||
--chart-4: 166 166 159;
|
||||
/* Chart quinary */
|
||||
--chart-5: 47 47 44;
|
||||
/* Sidebar background */
|
||||
--sidebar: 240 240 236;
|
||||
/* Sidebar text */
|
||||
--sidebar-foreground: 22 22 20;
|
||||
/* Sidebar primary accent */
|
||||
--sidebar-primary: 217 94 42;
|
||||
/* Sidebar primary text */
|
||||
--sidebar-primary-foreground: 249 249 247;
|
||||
/* Sidebar accent/hover */
|
||||
--sidebar-accent: 196 196 189;
|
||||
/* Sidebar accent text */
|
||||
--sidebar-accent-foreground: 22 22 20;
|
||||
/* Sidebar border */
|
||||
--sidebar-border: 196 196 189;
|
||||
/* Sidebar focus ring */
|
||||
--sidebar-ring: 217 94 42;
|
||||
}
|
||||
144
tokens/build/tokens.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
// Auto-generated by Style Dictionary — DO NOT EDIT
|
||||
// Source: tokens/*.json (W3C DTCG format)
|
||||
|
||||
export const ColorTokens = {
|
||||
'primitive.off-white': '#f9f9f7',
|
||||
'primitive.off-black': '#161614',
|
||||
'primitive.orange': '#d95e2a',
|
||||
'primitive.destructive-red': '#b43232',
|
||||
'primitive.grey.1': '#f0f0ec',
|
||||
'primitive.grey.2': '#ddddd7',
|
||||
'primitive.grey.3': '#c4c4bd',
|
||||
'primitive.grey.4': '#a6a69f',
|
||||
'primitive.grey.5': '#7f7f79',
|
||||
'primitive.grey.7': '#575753',
|
||||
'primitive.grey.8': '#2f2f2c',
|
||||
'semantic.background': '#f0f0ec',
|
||||
'semantic.foreground': '#161614',
|
||||
'semantic.card': '#f9f9f7',
|
||||
'semantic.card-foreground': '#161614',
|
||||
'semantic.popover': '#f9f9f7',
|
||||
'semantic.popover-foreground': '#161614',
|
||||
'semantic.primary': '#d95e2a',
|
||||
'semantic.primary-foreground': '#f9f9f7',
|
||||
'semantic.secondary': '#f0f0ec',
|
||||
'semantic.secondary-foreground': '#2f2f2c',
|
||||
'semantic.muted': '#f0f0ec',
|
||||
'semantic.muted-foreground': '#575753',
|
||||
'semantic.accent': '#ddddd7',
|
||||
'semantic.accent-foreground': '#161614',
|
||||
'semantic.destructive': '#b43232',
|
||||
'semantic.destructive-foreground': '#f9f9f7',
|
||||
'semantic.border': '#c4c4bd',
|
||||
'semantic.input': '#c4c4bd',
|
||||
'semantic.ring': '#d95e2a',
|
||||
'semantic.chart.1': '#d95e2a',
|
||||
'semantic.chart.2': '#575753',
|
||||
'semantic.chart.3': '#7f7f79',
|
||||
'semantic.chart.4': '#a6a69f',
|
||||
'semantic.chart.5': '#2f2f2c',
|
||||
'semantic.sidebar.background': '#f0f0ec',
|
||||
'semantic.sidebar.foreground': '#161614',
|
||||
'semantic.sidebar.primary': '#d95e2a',
|
||||
'semantic.sidebar.primary-foreground': '#f9f9f7',
|
||||
'semantic.sidebar.accent': '#c4c4bd',
|
||||
'semantic.sidebar.accent-foreground': '#161614',
|
||||
'semantic.sidebar.border': '#c4c4bd',
|
||||
'semantic.sidebar.ring': '#d95e2a',
|
||||
'dark.background': '#161614',
|
||||
'dark.foreground': '#f9f9f7',
|
||||
'dark.card': '#2f2f2c',
|
||||
'dark.card-foreground': '#f9f9f7',
|
||||
'dark.popover': '#2f2f2c',
|
||||
'dark.popover-foreground': '#f9f9f7',
|
||||
'dark.primary': '#d95e2a',
|
||||
'dark.primary-foreground': '#f9f9f7',
|
||||
'dark.secondary': '#575753',
|
||||
'dark.secondary-foreground': '#f9f9f7',
|
||||
'dark.muted': '#575753',
|
||||
'dark.muted-foreground': '#c4c4bd',
|
||||
'dark.accent': '#575753',
|
||||
'dark.accent-foreground': '#f9f9f7',
|
||||
'dark.destructive': '#b43232',
|
||||
'dark.destructive-foreground': '#f9f9f7',
|
||||
'dark.border': '#575753',
|
||||
'dark.input': '#575753',
|
||||
'dark.ring': '#d95e2a',
|
||||
'dark.chart.1': '#d95e2a',
|
||||
'dark.chart.2': '#c4c4bd',
|
||||
'dark.chart.3': '#a6a69f',
|
||||
'dark.chart.4': '#7f7f79',
|
||||
'dark.chart.5': '#f0f0ec',
|
||||
'dark.sidebar.background': '#2f2f2c',
|
||||
'dark.sidebar.foreground': '#f9f9f7',
|
||||
'dark.sidebar.primary': '#d95e2a',
|
||||
'dark.sidebar.primary-foreground': '#f9f9f7',
|
||||
'dark.sidebar.accent': '#575753',
|
||||
'dark.sidebar.accent-foreground': '#f9f9f7',
|
||||
'dark.sidebar.border': '#575753',
|
||||
'dark.sidebar.ring': '#d95e2a',
|
||||
} as const;
|
||||
|
||||
export const MotionTokens = {
|
||||
'duration.fast': '150ms',
|
||||
'duration.normal': '200ms',
|
||||
'duration.slow': '300ms',
|
||||
} as const;
|
||||
|
||||
export const RadiiTokens = {
|
||||
'base': '0.375rem',
|
||||
'sm': 'calc(0.375rem - 2px)',
|
||||
'md': '0.375rem',
|
||||
'lg': 'calc(0.375rem + 2px)',
|
||||
'xl': 'calc(0.375rem + 4px)',
|
||||
'full': '9999px',
|
||||
} as const;
|
||||
|
||||
export const ShadowTokens = {
|
||||
} as const;
|
||||
|
||||
export const SpacingTokens = {
|
||||
'0': '0',
|
||||
'1': '0.25rem',
|
||||
'2': '0.5rem',
|
||||
'3': '0.75rem',
|
||||
'4': '1rem',
|
||||
'5': '1.25rem',
|
||||
'6': '1.5rem',
|
||||
'8': '2rem',
|
||||
'10': '2.5rem',
|
||||
'12': '3rem',
|
||||
'16': '4rem',
|
||||
'20': '5rem',
|
||||
'24': '6rem',
|
||||
'0.5': '0.125rem',
|
||||
'1.5': '0.375rem',
|
||||
'component.card-padding': '1.5rem',
|
||||
'component.card-gap': '1.5rem',
|
||||
'component.section-padding': '4rem',
|
||||
'component.form-gap': '1rem',
|
||||
'component.button-padding-x': '1rem',
|
||||
'component.navbar-height': '4rem',
|
||||
} as const;
|
||||
|
||||
export const TypographyTokens = {
|
||||
'fontSize.xs': '0.75rem',
|
||||
'fontSize.sm': '0.875rem',
|
||||
'fontSize.base': '1rem',
|
||||
'fontSize.lg': '1.125rem',
|
||||
'fontSize.xl': '1.25rem',
|
||||
'fontSize.2xl': '1.5rem',
|
||||
'fontSize.3xl': '1.875rem',
|
||||
'fontSize.4xl': '2.25rem',
|
||||
'fontSize.5xl': '3rem',
|
||||
'fontWeight.normal': '400',
|
||||
'fontWeight.medium': '500',
|
||||
'fontWeight.semibold': '600',
|
||||
'fontWeight.bold': '700',
|
||||
'lineHeight.tight': '1.25rem',
|
||||
'lineHeight.normal': '1.5rem',
|
||||
'lineHeight.relaxed': '1.625rem',
|
||||
'letterSpacing.tight': '-0.025em',
|
||||
'letterSpacing.normal': '0em',
|
||||
'letterSpacing.wide': '0.05em',
|
||||
} as const;
|
||||
409
tokens/color.json
Normal file
@@ -0,0 +1,409 @@
|
||||
{
|
||||
"color": {
|
||||
"$description": "Greyhaven Design System — Color Tokens",
|
||||
"primitive": {
|
||||
"$description": "Raw color values — reference only, use semantic tokens in components",
|
||||
"off-white": {
|
||||
"$type": "color",
|
||||
"$value": "#F9F9F7",
|
||||
"$description": "Primary light surface — cards, elevated areas"
|
||||
},
|
||||
"off-black": {
|
||||
"$type": "color",
|
||||
"$value": "#161614",
|
||||
"$description": "Primary dark — foreground text, dark mode background"
|
||||
},
|
||||
"orange": {
|
||||
"$type": "color",
|
||||
"$value": "#D95E2A",
|
||||
"$description": "Only accent color — used sparingly for primary actions and emphasis"
|
||||
},
|
||||
"destructive-red": {
|
||||
"$type": "color",
|
||||
"$value": "#B43232",
|
||||
"$description": "Error/danger states"
|
||||
},
|
||||
"grey": {
|
||||
"1": {
|
||||
"$type": "color",
|
||||
"$value": "#F0F0EC",
|
||||
"$description": "5% — Subtle backgrounds, secondary, muted"
|
||||
},
|
||||
"2": {
|
||||
"$type": "color",
|
||||
"$value": "#DDDDD7",
|
||||
"$description": "10% — Accent hover, light borders"
|
||||
},
|
||||
"3": {
|
||||
"$type": "color",
|
||||
"$value": "#C4C4BD",
|
||||
"$description": "20% — Border, input"
|
||||
},
|
||||
"4": {
|
||||
"$type": "color",
|
||||
"$value": "#A6A69F",
|
||||
"$description": "50% — Mid-tone"
|
||||
},
|
||||
"5": {
|
||||
"$type": "color",
|
||||
"$value": "#7F7F79",
|
||||
"$description": "60% — Mid-dark"
|
||||
},
|
||||
"7": {
|
||||
"$type": "color",
|
||||
"$value": "#575753",
|
||||
"$description": "70% — Secondary foreground, muted foreground"
|
||||
},
|
||||
"8": {
|
||||
"$type": "color",
|
||||
"$value": "#2F2F2C",
|
||||
"$description": "80% — Dark mode card, dark surfaces"
|
||||
}
|
||||
}
|
||||
},
|
||||
"semantic": {
|
||||
"$description": "Semantic color assignments for light theme",
|
||||
"background": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.1}",
|
||||
"$description": "Page background"
|
||||
},
|
||||
"foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-black}",
|
||||
"$description": "Primary text"
|
||||
},
|
||||
"card": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Card/elevated surface background"
|
||||
},
|
||||
"card-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-black}",
|
||||
"$description": "Card text"
|
||||
},
|
||||
"popover": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Popover background"
|
||||
},
|
||||
"popover-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-black}",
|
||||
"$description": "Popover text"
|
||||
},
|
||||
"primary": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Primary accent — buttons, links, focus rings"
|
||||
},
|
||||
"primary-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Text on primary accent"
|
||||
},
|
||||
"secondary": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.1}",
|
||||
"$description": "Secondary button/surface"
|
||||
},
|
||||
"secondary-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.8}",
|
||||
"$description": "Text on secondary surface"
|
||||
},
|
||||
"muted": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.1}",
|
||||
"$description": "Muted/subdued background"
|
||||
},
|
||||
"muted-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Muted/subdued text"
|
||||
},
|
||||
"accent": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.2}",
|
||||
"$description": "Subtle hover state"
|
||||
},
|
||||
"accent-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-black}",
|
||||
"$description": "Text on accent hover"
|
||||
},
|
||||
"destructive": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.destructive-red}",
|
||||
"$description": "Destructive/error actions"
|
||||
},
|
||||
"destructive-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Text on destructive"
|
||||
},
|
||||
"border": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.3}",
|
||||
"$description": "Default border"
|
||||
},
|
||||
"input": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.3}",
|
||||
"$description": "Input border"
|
||||
},
|
||||
"ring": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Focus ring"
|
||||
},
|
||||
"hero-bg": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.2}",
|
||||
"$description": "Hero banner background"
|
||||
},
|
||||
"chart": {
|
||||
"1": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Chart accent"
|
||||
},
|
||||
"2": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Chart secondary"
|
||||
},
|
||||
"3": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.5}",
|
||||
"$description": "Chart tertiary"
|
||||
},
|
||||
"4": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.4}",
|
||||
"$description": "Chart quaternary"
|
||||
},
|
||||
"5": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.8}",
|
||||
"$description": "Chart quinary"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"background": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.1}",
|
||||
"$description": "Sidebar background"
|
||||
},
|
||||
"foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-black}",
|
||||
"$description": "Sidebar text"
|
||||
},
|
||||
"primary": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Sidebar primary accent"
|
||||
},
|
||||
"primary-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Sidebar primary text"
|
||||
},
|
||||
"accent": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.3}",
|
||||
"$description": "Sidebar accent/hover"
|
||||
},
|
||||
"accent-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-black}",
|
||||
"$description": "Sidebar accent text"
|
||||
},
|
||||
"border": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.3}",
|
||||
"$description": "Sidebar border"
|
||||
},
|
||||
"ring": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Sidebar focus ring"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"$description": "Dark theme overrides — inverted warm greys, same orange accent",
|
||||
"background": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-black}",
|
||||
"$description": "Dark page background"
|
||||
},
|
||||
"foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark primary text"
|
||||
},
|
||||
"card": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.8}",
|
||||
"$description": "Dark card surface"
|
||||
},
|
||||
"card-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark card text"
|
||||
},
|
||||
"popover": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.8}",
|
||||
"$description": "Dark popover"
|
||||
},
|
||||
"popover-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark popover text"
|
||||
},
|
||||
"primary": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Same orange in dark mode"
|
||||
},
|
||||
"primary-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark primary foreground"
|
||||
},
|
||||
"secondary": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Dark secondary"
|
||||
},
|
||||
"secondary-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark secondary text"
|
||||
},
|
||||
"muted": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.8}",
|
||||
"$description": "Dark muted — grey.8 (distinct from grey.7 border so outlines on bg-muted surfaces remain visible)"
|
||||
},
|
||||
"muted-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.3}",
|
||||
"$description": "Dark muted text"
|
||||
},
|
||||
"accent": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Dark accent/hover"
|
||||
},
|
||||
"accent-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark accent text"
|
||||
},
|
||||
"destructive": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.destructive-red}",
|
||||
"$description": "Same destructive in dark mode"
|
||||
},
|
||||
"destructive-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark destructive text"
|
||||
},
|
||||
"border": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Dark border"
|
||||
},
|
||||
"input": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Dark input border"
|
||||
},
|
||||
"ring": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Dark focus ring"
|
||||
},
|
||||
"hero-bg": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.8}",
|
||||
"$description": "Dark hero banner background"
|
||||
},
|
||||
"chart": {
|
||||
"1": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Dark chart accent"
|
||||
},
|
||||
"2": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.3}",
|
||||
"$description": "Dark chart secondary"
|
||||
},
|
||||
"3": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.4}",
|
||||
"$description": "Dark chart tertiary"
|
||||
},
|
||||
"4": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.5}",
|
||||
"$description": "Dark chart quaternary"
|
||||
},
|
||||
"5": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.1}",
|
||||
"$description": "Dark chart quinary"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"background": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.8}",
|
||||
"$description": "Dark sidebar background"
|
||||
},
|
||||
"foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark sidebar text"
|
||||
},
|
||||
"primary": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Dark sidebar primary"
|
||||
},
|
||||
"primary-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark sidebar primary text"
|
||||
},
|
||||
"accent": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Dark sidebar accent"
|
||||
},
|
||||
"accent-foreground": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.off-white}",
|
||||
"$description": "Dark sidebar accent text"
|
||||
},
|
||||
"border": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.grey.7}",
|
||||
"$description": "Dark sidebar border"
|
||||
},
|
||||
"ring": {
|
||||
"$type": "color",
|
||||
"$value": "{color.primitive.orange}",
|
||||
"$description": "Dark sidebar ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
tokens/motion.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"motion": {
|
||||
"$description": "Greyhaven Design System — Animation & Motion Tokens",
|
||||
"duration": {
|
||||
"fast": {
|
||||
"$type": "duration",
|
||||
"$value": "150ms",
|
||||
"$description": "Quick transitions — tooltips, hover states"
|
||||
},
|
||||
"normal": {
|
||||
"$type": "duration",
|
||||
"$value": "200ms",
|
||||
"$description": "Default transitions — most UI interactions"
|
||||
},
|
||||
"slow": {
|
||||
"$type": "duration",
|
||||
"$value": "300ms",
|
||||
"$description": "Deliberate transitions — modals, drawers, accordions"
|
||||
}
|
||||
},
|
||||
"easing": {
|
||||
"default": {
|
||||
"$type": "cubicBezier",
|
||||
"$value": [0.4, 0, 0.2, 1],
|
||||
"$description": "Standard ease-in-out"
|
||||
},
|
||||
"in": {
|
||||
"$type": "cubicBezier",
|
||||
"$value": [0.4, 0, 1, 1],
|
||||
"$description": "Ease-in for exits"
|
||||
},
|
||||
"out": {
|
||||
"$type": "cubicBezier",
|
||||
"$value": [0, 0, 0.2, 1],
|
||||
"$description": "Ease-out for entrances"
|
||||
}
|
||||
},
|
||||
"keyframes": {
|
||||
"$description": "Named animation patterns used by components",
|
||||
"accordion-down": {
|
||||
"$description": "Accordion open — height 0 to auto"
|
||||
},
|
||||
"accordion-up": {
|
||||
"$description": "Accordion close — height auto to 0"
|
||||
},
|
||||
"spin": {
|
||||
"$description": "Spinner rotation — 360deg continuous"
|
||||
},
|
||||
"pulse": {
|
||||
"$description": "Skeleton loading — opacity fade in/out"
|
||||
},
|
||||
"fade-in": {
|
||||
"$description": "Generic entrance — opacity 0 to 1"
|
||||
},
|
||||
"fade-out": {
|
||||
"$description": "Generic exit — opacity 1 to 0"
|
||||
},
|
||||
"slide-in-from-top": {
|
||||
"$description": "Dropdown entrance — translate-y negative to 0"
|
||||
},
|
||||
"slide-in-from-bottom": {
|
||||
"$description": "Drawer entrance — translate-y positive to 0"
|
||||
},
|
||||
"zoom-in": {
|
||||
"$description": "Dialog entrance — scale 0.95 to 1 + fade"
|
||||
},
|
||||
"zoom-out": {
|
||||
"$description": "Dialog exit — scale 1 to 0.95 + fade"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
tokens/radii.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"radii": {
|
||||
"$description": "Greyhaven Design System — Border Radius Tokens (tightened, no playful rounding)",
|
||||
"base": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.375rem",
|
||||
"$description": "6px — base radius"
|
||||
},
|
||||
"sm": {
|
||||
"$type": "dimension",
|
||||
"$value": "calc(0.375rem - 2px)",
|
||||
"$description": "4px — small variant"
|
||||
},
|
||||
"md": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.375rem",
|
||||
"$description": "6px — medium (same as base)"
|
||||
},
|
||||
"lg": {
|
||||
"$type": "dimension",
|
||||
"$value": "calc(0.375rem + 2px)",
|
||||
"$description": "8px — large variant"
|
||||
},
|
||||
"xl": {
|
||||
"$type": "dimension",
|
||||
"$value": "calc(0.375rem + 4px)",
|
||||
"$description": "10px — extra large variant (cards)"
|
||||
},
|
||||
"full": {
|
||||
"$type": "dimension",
|
||||
"$value": "9999px",
|
||||
"$description": "Fully round (pills, avatars)"
|
||||
}
|
||||
}
|
||||
}
|
||||
49
tokens/shadows.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"shadow": {
|
||||
"$description": "Greyhaven Design System — Shadow Tokens (subtle, warm-neutral)",
|
||||
"xs": {
|
||||
"$type": "shadow",
|
||||
"$value": {
|
||||
"offsetX": "0",
|
||||
"offsetY": "1px",
|
||||
"blur": "2px",
|
||||
"spread": "0",
|
||||
"color": "rgba(22, 22, 20, 0.05)"
|
||||
},
|
||||
"$description": "Subtle shadow for buttons, inputs"
|
||||
},
|
||||
"sm": {
|
||||
"$type": "shadow",
|
||||
"$value": {
|
||||
"offsetX": "0",
|
||||
"offsetY": "1px",
|
||||
"blur": "3px",
|
||||
"spread": "0",
|
||||
"color": "rgba(22, 22, 20, 0.1)"
|
||||
},
|
||||
"$description": "Small shadow for cards"
|
||||
},
|
||||
"md": {
|
||||
"$type": "shadow",
|
||||
"$value": {
|
||||
"offsetX": "0",
|
||||
"offsetY": "4px",
|
||||
"blur": "6px",
|
||||
"spread": "-1px",
|
||||
"color": "rgba(22, 22, 20, 0.1)"
|
||||
},
|
||||
"$description": "Medium shadow for dropdowns, popovers"
|
||||
},
|
||||
"lg": {
|
||||
"$type": "shadow",
|
||||
"$value": {
|
||||
"offsetX": "0",
|
||||
"offsetY": "10px",
|
||||
"blur": "15px",
|
||||
"spread": "-3px",
|
||||
"color": "rgba(22, 22, 20, 0.1)"
|
||||
},
|
||||
"$description": "Large shadow for dialogs, modals"
|
||||
}
|
||||
}
|
||||
}
|
||||
113
tokens/spacing.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"spacing": {
|
||||
"$description": "Greyhaven Design System — Spacing Tokens (derived from component usage patterns)",
|
||||
"0": {
|
||||
"$type": "dimension",
|
||||
"$value": "0",
|
||||
"$description": "None"
|
||||
},
|
||||
"0.5": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.125rem",
|
||||
"$description": "2px — micro spacing"
|
||||
},
|
||||
"1": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.25rem",
|
||||
"$description": "4px — tight gaps"
|
||||
},
|
||||
"1.5": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.375rem",
|
||||
"$description": "6px"
|
||||
},
|
||||
"2": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.5rem",
|
||||
"$description": "8px — card header gap, form description spacing"
|
||||
},
|
||||
"3": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.75rem",
|
||||
"$description": "12px"
|
||||
},
|
||||
"4": {
|
||||
"$type": "dimension",
|
||||
"$value": "1rem",
|
||||
"$description": "16px — form field gap, button padding"
|
||||
},
|
||||
"5": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.25rem",
|
||||
"$description": "20px"
|
||||
},
|
||||
"6": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.5rem",
|
||||
"$description": "24px — card padding, card internal gap"
|
||||
},
|
||||
"8": {
|
||||
"$type": "dimension",
|
||||
"$value": "2rem",
|
||||
"$description": "32px — section margin-bottom"
|
||||
},
|
||||
"10": {
|
||||
"$type": "dimension",
|
||||
"$value": "2.5rem",
|
||||
"$description": "40px"
|
||||
},
|
||||
"12": {
|
||||
"$type": "dimension",
|
||||
"$value": "3rem",
|
||||
"$description": "48px"
|
||||
},
|
||||
"16": {
|
||||
"$type": "dimension",
|
||||
"$value": "4rem",
|
||||
"$description": "64px — major section padding (py-16)"
|
||||
},
|
||||
"20": {
|
||||
"$type": "dimension",
|
||||
"$value": "5rem",
|
||||
"$description": "80px"
|
||||
},
|
||||
"24": {
|
||||
"$type": "dimension",
|
||||
"$value": "6rem",
|
||||
"$description": "96px — hero padding"
|
||||
},
|
||||
"component": {
|
||||
"$description": "Component-specific spacing patterns",
|
||||
"card-padding": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.5rem",
|
||||
"$description": "Card internal padding (px-6)"
|
||||
},
|
||||
"card-gap": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.5rem",
|
||||
"$description": "Gap between cards (gap-6)"
|
||||
},
|
||||
"section-padding": {
|
||||
"$type": "dimension",
|
||||
"$value": "2.5rem",
|
||||
"$description": "Vertical padding inside sections (py-10)"
|
||||
},
|
||||
"form-gap": {
|
||||
"$type": "dimension",
|
||||
"$value": "1rem",
|
||||
"$description": "Gap between form fields (gap-4)"
|
||||
},
|
||||
"button-padding-x": {
|
||||
"$type": "dimension",
|
||||
"$value": "1rem",
|
||||
"$description": "Button horizontal padding (px-4)"
|
||||
},
|
||||
"navbar-height": {
|
||||
"$type": "dimension",
|
||||
"$value": "4rem",
|
||||
"$description": "Navbar height (h-16)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
155
tokens/typography.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"typography": {
|
||||
"$description": "Greyhaven Design System — Typography Tokens",
|
||||
"fontFamily": {
|
||||
"sans": {
|
||||
"$type": "fontFamily",
|
||||
"$value": ["Aspekta", "ui-sans-serif", "system-ui", "sans-serif"],
|
||||
"$description": "UI labels, buttons, nav, forms — Aspekta self-hosted"
|
||||
},
|
||||
"serif": {
|
||||
"$type": "fontFamily",
|
||||
"$value": ["Source Serif 4", "Source Serif Pro", "Georgia", "serif"],
|
||||
"$description": "Headings, body content, reading — Source Serif primary"
|
||||
},
|
||||
"mono": {
|
||||
"$type": "fontFamily",
|
||||
"$value": ["ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "monospace"],
|
||||
"$description": "Code blocks and monospaced content"
|
||||
}
|
||||
},
|
||||
"fontSize": {
|
||||
"xs": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.75rem",
|
||||
"$description": "12px — metadata, fine print"
|
||||
},
|
||||
"sm": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.875rem",
|
||||
"$description": "14px — captions, nav, labels, buttons"
|
||||
},
|
||||
"base": {
|
||||
"$type": "dimension",
|
||||
"$value": "1rem",
|
||||
"$description": "16px — body text"
|
||||
},
|
||||
"lg": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.125rem",
|
||||
"$description": "18px — large body, subtitles"
|
||||
},
|
||||
"xl": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.25rem",
|
||||
"$description": "20px — H3"
|
||||
},
|
||||
"2xl": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.5rem",
|
||||
"$description": "24px — H2"
|
||||
},
|
||||
"3xl": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.875rem",
|
||||
"$description": "30px — large H2"
|
||||
},
|
||||
"4xl": {
|
||||
"$type": "dimension",
|
||||
"$value": "2.25rem",
|
||||
"$description": "36px — H1"
|
||||
},
|
||||
"5xl": {
|
||||
"$type": "dimension",
|
||||
"$value": "3rem",
|
||||
"$description": "48px — hero heading"
|
||||
}
|
||||
},
|
||||
"fontWeight": {
|
||||
"normal": {
|
||||
"$type": "fontWeight",
|
||||
"$value": 400,
|
||||
"$description": "Regular body text"
|
||||
},
|
||||
"medium": {
|
||||
"$type": "fontWeight",
|
||||
"$value": 500,
|
||||
"$description": "H3, labels, nav items"
|
||||
},
|
||||
"semibold": {
|
||||
"$type": "fontWeight",
|
||||
"$value": 600,
|
||||
"$description": "H1, H2, buttons"
|
||||
},
|
||||
"bold": {
|
||||
"$type": "fontWeight",
|
||||
"$value": 700,
|
||||
"$description": "Strong emphasis"
|
||||
}
|
||||
},
|
||||
"lineHeight": {
|
||||
"tight": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.25",
|
||||
"$description": "Headings"
|
||||
},
|
||||
"normal": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.5",
|
||||
"$description": "Default"
|
||||
},
|
||||
"relaxed": {
|
||||
"$type": "dimension",
|
||||
"$value": "1.625",
|
||||
"$description": "Body content for readability"
|
||||
}
|
||||
},
|
||||
"letterSpacing": {
|
||||
"tight": {
|
||||
"$type": "dimension",
|
||||
"$value": "-0.025em",
|
||||
"$description": "Headings — tracking-tight"
|
||||
},
|
||||
"normal": {
|
||||
"$type": "dimension",
|
||||
"$value": "0em",
|
||||
"$description": "Body text"
|
||||
},
|
||||
"wide": {
|
||||
"$type": "dimension",
|
||||
"$value": "0.05em",
|
||||
"$description": "Uppercase labels"
|
||||
}
|
||||
},
|
||||
"composite": {
|
||||
"$description": "Pre-composed typography styles for common use cases",
|
||||
"h1": {
|
||||
"$description": "Page heading — serif, text-4xl, font-semibold, tracking-tight"
|
||||
},
|
||||
"h2": {
|
||||
"$description": "Section heading — serif, text-2xl, font-semibold, tracking-tight"
|
||||
},
|
||||
"h3": {
|
||||
"$description": "Subsection heading — serif, text-xl, font-medium"
|
||||
},
|
||||
"body": {
|
||||
"$description": "Body text — serif, text-base, leading-relaxed"
|
||||
},
|
||||
"caption": {
|
||||
"$description": "Caption text — sans, text-sm"
|
||||
},
|
||||
"nav": {
|
||||
"$description": "Navigation — sans, text-sm, font-medium"
|
||||
},
|
||||
"button": {
|
||||
"$description": "Button label — sans, text-sm, font-semibold"
|
||||
},
|
||||
"label": {
|
||||
"$description": "Form label — sans, text-sm, font-medium"
|
||||
},
|
||||
"metadata": {
|
||||
"$description": "Metadata — sans, text-xs, font-medium"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
vitest.config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
||||
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
|
||||
const dirname =
|
||||
typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
||||
export default defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
plugins: [
|
||||
// The plugin will run tests for the stories defined in your Storybook config
|
||||
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
||||
storybookTest({ configDir: path.join(dirname, '.storybook') }),
|
||||
],
|
||||
test: {
|
||||
name: 'storybook',
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright({}),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
1
vitest.shims.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@vitest/browser-playwright" />
|
||||