From c3215945f22faae0c7b44ecaa2b9aca2a4505a8a Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 13 Apr 2026 15:33:00 -0500 Subject: [PATCH] design system token v0.1 --- .gitignore | 7 +- .storybook/main.ts | 17 + .storybook/preview.ts | 44 + README.md | 212 +- app/globals.css | 144 +- app/tokens/TOKENS.md | 168 + app/tokens/tokens-dark.css | 70 + app/tokens/tokens-light.css | 70 + app/tokens/tokens.ts | 144 + components/theme-provider.tsx | 88 +- components/ui/cta-section.tsx | 86 + components/ui/footer.tsx | 109 + components/ui/hero.tsx | 94 + components/ui/logo.tsx | 92 + components/ui/navbar.tsx | 131 + components/ui/page-layout.tsx | 52 + components/ui/section.tsx | 69 + components/ui/sonner.tsx | 6 +- lib/catalog.ts | 430 ++ mcp/server.ts | 304 ++ mcp/tsconfig.json | 14 + next.config.mjs | 3 + package.json | 29 +- pnpm-lock.yaml | 4403 +++++++++++++++++++- scripts/generate-skill.ts | 250 ++ skill/SKILL.md | 662 +++ skill/install.sh | 68 + stories/Composition/CTASection.stories.tsx | 111 + stories/Composition/Footer.stories.tsx | 92 + stories/Composition/Hero.stories.tsx | 111 + stories/Composition/PageLayout.stories.tsx | 167 + stories/Composition/Section.stories.tsx | 135 + stories/Data/Progress.stories.tsx | 88 + stories/Data/Table.stories.tsx | 117 + stories/Feedback/Alert.stories.tsx | 71 + stories/Feedback/Skeleton.stories.tsx | 63 + stories/Feedback/Spinner.stories.tsx | 54 + stories/Form/Form.stories.tsx | 211 + stories/Layout/Accordion.stories.tsx | 87 + stories/Layout/Card.stories.tsx | 97 + stories/Layout/Separator.stories.tsx | 59 + stories/Navigation/Breadcrumb.stories.tsx | 109 + stories/Navigation/Navbar.stories.tsx | 94 + stories/Overlay/AlertDialog.stories.tsx | 72 + stories/Overlay/Dialog.stories.tsx | 106 + stories/Overlay/Tooltip.stories.tsx | 99 + stories/Primitives/Badge.stories.tsx | 170 + stories/Primitives/Button.stories.tsx | 171 + stories/Primitives/Input.stories.tsx | 88 + stories/Primitives/Toggle.stories.tsx | 103 + style-dictionary.config.mjs | 190 + tokens/build/TOKENS.md | 168 + tokens/build/tokens-dark.css | 70 + tokens/build/tokens-light.css | 70 + tokens/build/tokens.ts | 144 + tokens/color.json | 399 ++ tokens/motion.json | 72 + tokens/radii.json | 35 + tokens/shadows.json | 49 + tokens/spacing.json | 113 + tokens/typography.json | 155 + vitest.config.ts | 36 + vitest.shims.d.ts | 1 + 63 files changed, 11562 insertions(+), 181 deletions(-) create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview.ts create mode 100644 app/tokens/TOKENS.md create mode 100644 app/tokens/tokens-dark.css create mode 100644 app/tokens/tokens-light.css create mode 100644 app/tokens/tokens.ts create mode 100644 components/ui/cta-section.tsx create mode 100644 components/ui/footer.tsx create mode 100644 components/ui/hero.tsx create mode 100644 components/ui/logo.tsx create mode 100644 components/ui/navbar.tsx create mode 100644 components/ui/page-layout.tsx create mode 100644 components/ui/section.tsx create mode 100644 lib/catalog.ts create mode 100644 mcp/server.ts create mode 100644 mcp/tsconfig.json create mode 100644 scripts/generate-skill.ts create mode 100644 skill/SKILL.md create mode 100755 skill/install.sh create mode 100644 stories/Composition/CTASection.stories.tsx create mode 100644 stories/Composition/Footer.stories.tsx create mode 100644 stories/Composition/Hero.stories.tsx create mode 100644 stories/Composition/PageLayout.stories.tsx create mode 100644 stories/Composition/Section.stories.tsx create mode 100644 stories/Data/Progress.stories.tsx create mode 100644 stories/Data/Table.stories.tsx create mode 100644 stories/Feedback/Alert.stories.tsx create mode 100644 stories/Feedback/Skeleton.stories.tsx create mode 100644 stories/Feedback/Spinner.stories.tsx create mode 100644 stories/Form/Form.stories.tsx create mode 100644 stories/Layout/Accordion.stories.tsx create mode 100644 stories/Layout/Card.stories.tsx create mode 100644 stories/Layout/Separator.stories.tsx create mode 100644 stories/Navigation/Breadcrumb.stories.tsx create mode 100644 stories/Navigation/Navbar.stories.tsx create mode 100644 stories/Overlay/AlertDialog.stories.tsx create mode 100644 stories/Overlay/Dialog.stories.tsx create mode 100644 stories/Overlay/Tooltip.stories.tsx create mode 100644 stories/Primitives/Badge.stories.tsx create mode 100644 stories/Primitives/Button.stories.tsx create mode 100644 stories/Primitives/Input.stories.tsx create mode 100644 stories/Primitives/Toggle.stories.tsx create mode 100644 style-dictionary.config.mjs create mode 100644 tokens/build/TOKENS.md create mode 100644 tokens/build/tokens-dark.css create mode 100644 tokens/build/tokens-light.css create mode 100644 tokens/build/tokens.ts create mode 100644 tokens/color.json create mode 100644 tokens/motion.json create mode 100644 tokens/radii.json create mode 100644 tokens/shadows.json create mode 100644 tokens/spacing.json create mode 100644 tokens/typography.json create mode 100644 vitest.config.ts create mode 100644 vitest.shims.d.ts diff --git a/.gitignore b/.gitignore index f650315..cfa1e15 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,9 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts +*storybook.log +storybook-static + +# llms +vibedocs/* diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..51fe116 --- /dev/null +++ b/.storybook/main.ts @@ -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; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..d380385 --- /dev/null +++ b/.storybook/preview.ts @@ -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 diff --git a/README.md b/README.md index f27ad74..73ce57f 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,201 @@ # 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. ![Screenshot](docs/screenshot.png) -## 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/ # Claude Skill (auto-generated) +│ ├── SKILL.md +│ └── install.sh +├── 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 two complementary ways for AI agents to consume it: + +| | Claude Skill (SKILL.md) | MCP Server | +|---|---|---| +| **What it is** | A single markdown file with all tokens, components, and rules | A running process that reads source files in real-time | +| **Stays in sync** | Yes -- auto-generated from the same token files and component catalog | Yes -- reads source at runtime | +| **Setup** | Copy/symlink one file | Start a server process | +| **Best for** | Claude Code sessions, quick context | Programmatic access, validation, any MCP-compatible agent | + +Both read from the same sources (`tokens/*.json` and `lib/catalog.ts`), so they always agree. + +### Option A: Claude Skill (SKILL.md) + +The skill file gives any Claude Code session full design system context -- tokens, all components with props/examples, composition rules, and the extension protocol. + +**Install into a consuming project:** + +```bash +# From the design system repo +./skill/install.sh /path/to/your/project + +# Or manually +mkdir -p /path/to/your/project/.claude/skills +ln -sf /absolute/path/to/greyhaven-design-system/skill/SKILL.md \ + /path/to/your/project/.claude/skills/greyhaven.md +``` + +**Regenerate after changes:** + +```bash +pnpm skill:build +``` + +This is run automatically as part of `pnpm build`. If you add a component, add it to `lib/catalog.ts` and regenerate. + +### Option B: MCP Server + +The MCP server provides 5 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 | + +Plus resources: `tokens://all` and `component://{name}` for each component. + +**Run directly:** + +```bash +pnpm mcp:start +``` + +**Install in Claude Code (`~/.claude/settings.json`):** + +```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//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 from tokens + catalog | +| `pnpm mcp:start` | Start the MCP server (stdio transport) | +| `pnpm mcp:build` | Type-check MCP server | +| `pnpm lint` | Run ESLint | diff --git a/app/globals.css b/app/globals.css index a0cb00b..49be907 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,12 +1,17 @@ @import 'tailwindcss'; @import 'tw-animate-css'; +@import './tokens/tokens-light.css'; +@import './tokens/tokens-dark.css'; @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 +19,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 is the canonical sans font, Inter is fallback */ + --font-sans: 'Aspekta', 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; --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)); @@ -186,7 +72,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; diff --git a/app/tokens/TOKENS.md b/app/tokens/TOKENS.md new file mode 100644 index 0000000..de5168b --- /dev/null +++ b/app/tokens/TOKENS.md @@ -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 | diff --git a/app/tokens/tokens-dark.css b/app/tokens/tokens-dark.css new file mode 100644 index 0000000..9bdd5f2 --- /dev/null +++ b/app/tokens/tokens-dark.css @@ -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; +} \ No newline at end of file diff --git a/app/tokens/tokens-light.css b/app/tokens/tokens-light.css new file mode 100644 index 0000000..14f2bed --- /dev/null +++ b/app/tokens/tokens-light.css @@ -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; +} \ No newline at end of file diff --git a/app/tokens/tokens.ts b/app/tokens/tokens.ts new file mode 100644 index 0000000..4a8db66 --- /dev/null +++ b/app/tokens/tokens.ts @@ -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; diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 55c2f6e..1cdda07 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -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 {children} +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(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(() => { + 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 ( + + {children} + + ) +} + +export function useTheme() { + const context = React.useContext(ThemeContext) + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context } diff --git a/components/ui/cta-section.tsx b/components/ui/cta-section.tsx new file mode 100644 index 0000000..5951dd1 --- /dev/null +++ b/components/ui/cta-section.tsx @@ -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 { + heading: React.ReactNode + description?: React.ReactNode + actions?: React.ReactNode +} + +function CTASection({ + className, + variant, + background, + heading, + description, + actions, + children, + ...props +}: CTASectionProps) { + return ( +
+
+

+ {heading} +

+ {description && ( +

+ {description} +

+ )} + {actions && ( +
+ {actions} +
+ )} + {children} +
+
+ ) +} + +export { CTASection, ctaSectionVariants } diff --git a/components/ui/footer.tsx b/components/ui/footer.tsx new file mode 100644 index 0000000..349c73c --- /dev/null +++ b/components/ui/footer.tsx @@ -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 { + 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 ( +
+
+
+ {logo && ( +
+ {logo} +
+ )} + {linkGroups.map((group) => ( +
+

{group.title}

+ +
+ ))} +
+
+ {copyright && ( +

{copyright}

+ )} + {actions} +
+ {children} +
+
+ ) + } + + return ( +
+
+
+ {logo &&
{logo}
} + {copyright && ( +

{copyright}

+ )} + {actions} + {children} +
+
+
+ ) +} + +export { Footer, footerVariants } diff --git a/components/ui/hero.tsx b/components/ui/hero.tsx new file mode 100644 index 0000000..b3a232e --- /dev/null +++ b/components/ui/hero.tsx @@ -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-background', + muted: 'bg-muted', + accent: 'bg-primary/5', + dark: 'bg-foreground text-background', + }, + }, + defaultVariants: { + variant: 'centered', + background: 'default', + }, +}) + +interface HeroProps + extends React.ComponentProps<'section'>, + VariantProps { + 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 ( +
+
+
+

+ {heading} +

+ {subheading && ( +

+ {subheading} +

+ )} + {actions && ( +
+ {actions} +
+ )} + {children} +
+ {isSplit && media && ( +
{media}
+ )} +
+
+ ) +} + +export { Hero, heroVariants } diff --git a/components/ui/logo.tsx b/components/ui/logo.tsx new file mode 100644 index 0000000..bfcc9b8 --- /dev/null +++ b/components/ui/logo.tsx @@ -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) { + return ( + + + + + + + + + + + + + + + + + + + + ) +} + +export { Logo, logoVariants } diff --git a/components/ui/navbar.tsx b/components/ui/navbar.tsx new file mode 100644 index 0000000..503cf79 --- /dev/null +++ b/components/ui/navbar.tsx @@ -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-16 font-sans', + { + variants: { + variant: { + solid: 'bg-background border-b border-border', + transparent: 'bg-transparent', + minimal: 'bg-background/80 backdrop-blur-sm border-b border-border/50', + }, + }, + defaultVariants: { + variant: 'solid', + }, + }, +) + +interface NavbarProps + extends React.ComponentProps<'header'>, + VariantProps { + logo?: React.ReactNode + actions?: React.ReactNode +} + +function Navbar({ + className, + variant, + logo, + actions, + children, + ...props +}: NavbarProps) { + const [mobileOpen, setMobileOpen] = React.useState(false) + + return ( +
+
+ {/* Logo slot — left */} + {logo && ( +
+ {logo} +
+ )} + + {/* Desktop nav — center */} + + + {/* Actions slot — right */} +
+ {actions && ( +
+ {actions} +
+ )} + + {/* Mobile menu toggle */} + +
+
+ + {/* Mobile nav */} + {mobileOpen && ( +
+ + {actions && ( +
+ {actions} +
+ )} +
+ )} +
+ ) +} + +function NavbarLink({ + className, + active, + ...props +}: React.ComponentProps<'a'> & { active?: boolean }) { + return ( + + ) +} + +export { Navbar, NavbarLink, navbarVariants } diff --git a/components/ui/page-layout.tsx b/components/ui/page-layout.tsx new file mode 100644 index 0000000..f74ef68 --- /dev/null +++ b/components/ui/page-layout.tsx @@ -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 ( +
+ {navbar} +
+ {sidebar && ( + + )} +
+ {children} +
+
+ {footer} +
+ ) +} + +export { PageLayout } diff --git a/components/ui/section.tsx b/components/ui/section.tsx new file mode 100644 index 0000000..8181d54 --- /dev/null +++ b/components/ui/section.tsx @@ -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-16', { + variants: { + variant: { + default: '', + highlighted: 'bg-muted', + accent: 'bg-primary/5', + }, + 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 { + title?: string + description?: string +} + +function Section({ + className, + variant, + width, + title, + description, + children, + ...props +}: SectionProps) { + return ( +
+
+ {(title || description) && ( +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ )} + {children} +
+
+ ) +} + +export { Section, sectionVariants } diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx index 0626caf..2bbca07 100644 --- a/components/ui/sonner.tsx +++ b/components/ui/sonner.tsx @@ -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 ( { + const filePath = path.join(root, 'tokens', `${name}.json`) + return JSON.parse(fs.readFileSync(filePath, 'utf-8')) +} + +export function flattenTokens( + obj: Record, + prefix = '', +): FlatToken[] { + const results: FlatToken[] = [] + + for (const [key, val] of Object.entries(obj)) { + if (key.startsWith('$')) continue + const currentPath = prefix ? `${prefix}.${key}` : key + const node = val as Record + + if (node && typeof node === 'object' && '$value' in node) { + results.push({ + path: currentPath, + value: node.$value, + type: node.$type as string | undefined, + description: node.$description as string | undefined, + }) + } else if (node && typeof node === 'object') { + results.push(...flattenTokens(node, currentPath)) + } + } + + return results +} + +export function getTokens(root: string, category?: string): FlatToken[] { + if (category && TOKEN_CATEGORIES.includes(category as TokenCategory)) { + const data = loadTokenFile(root, category) + return flattenTokens(data) + } + const all: FlatToken[] = [] + for (const cat of TOKEN_CATEGORIES) { + try { + const data = loadTokenFile(root, cat) + all.push(...flattenTokens(data)) + } catch { + // skip missing files + } + } + return all +} + +// --------------------------------------------------------------------------- +// Component catalog +// --------------------------------------------------------------------------- + +export interface ComponentSpec { + name: string + file: string + category: string + exports: string[] + description: string + props: string + example: string +} + +export const COMPONENT_CATALOG: ComponentSpec[] = [ + // ── Primitives ────────────────────────────────────────────────────────── + { + name: 'Button', + file: 'components/ui/button.tsx', + category: 'primitives', + exports: ['Button', 'buttonVariants'], + description: 'Primary interactive element. 6 variants: default (orange), secondary, outline, ghost, link, destructive. Sizes: default (h-9), sm (h-8), lg (h-10), icon (size-9).', + props: 'variant?: "default" | "secondary" | "outline" | "ghost" | "link" | "destructive"; size?: "default" | "sm" | "lg" | "icon" | "icon-sm" | "icon-lg"; asChild?: boolean', + example: '', + }, + { + name: 'Badge', + file: 'components/ui/badge.tsx', + category: 'primitives', + exports: ['Badge', 'badgeVariants'], + description: 'Status indicator / tag. Variants include default, secondary, outline, destructive, success, warning, info, plus channel pills (WhatsApp, Email, Telegram, Zulip).', + props: 'variant?: "default" | "secondary" | "destructive" | "outline" | "success" | "warning" | "info" | ...; asChild?: boolean', + example: 'Active', + }, + { + name: 'Input', + file: 'components/ui/input.tsx', + category: 'primitives', + exports: ['Input'], + description: 'Text input field with focus ring, disabled, and aria-invalid states.', + props: 'All standard HTML input props', + example: '', + }, + { + name: 'Textarea', + file: 'components/ui/textarea.tsx', + category: 'primitives', + exports: ['Textarea'], + description: 'Multi-line text input.', + props: 'All standard HTML textarea props', + example: '