diff --git a/AGENTS.md b/AGENTS.md index 769cc44..f417e48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,9 +28,10 @@ React 19 + Vite 8 + TypeScript design-system showcase. Dark-first, token-driven ## Smart Utils & AI Workflow - **Class helper**: `src/utils/cn.ts` — zero-dependency classnames merging utility for conditional component styles. -- **Smart CSS**: `src/utils.css` — ready-to-use helpers (`.dot-canvas`, `.surface-card`, `.surface-sidebar`, `.badge-*`, `.hover-lift`, `.kbd-tag`, `.skeleton-pulse`). -- **Composable Primitives**: `src/components/ui/` (`Badge.tsx`, `Card.tsx`, `StatCard.tsx`, `Kbd.tsx`). -- **AI Agent System Prompt & Recipes**: `src/data/ai-rules.ts`. +- **Smart CSS**: `src/utils.css` — ready-to-use helpers (`.dot-canvas`, `.surface-card`, `.surface-sidebar`, `.badge-*`, `.hover-lift`, `.kbd-tag`, `.skeleton-pulse`, `.progress-*`, `.ui-table-*`, `.dialog-*`, `.sheet-*`, `.alert-*`, `.ui-tabs-*`, `.switch-*`). +- **Composable Primitives**: `src/components/ui/` (`Dialog`, `Sheet`, `Tabs`, `Alert`, `EmptyState`, `Table`, `Progress`, `Switch`, `Badge`, `Card`, `StatCard`, `Skeleton`, `Tooltip`, `Kbd`, `Icon`). +- **Reusable Application Patterns**: `src/components/examples/` (`MetricsDashboard`, `SettingsPanel`, `ServiceTable`, `DeploymentModal`). +- **Icon Library**: `lucide-react` — 1,400+ tree-shaken SVGs with uniform stroke and color inheritance (`` or direct import). ## Structure @@ -44,15 +45,16 @@ src/ utils/ cn.ts ← conditional classnames utility components/ - ui/ ← composable UI primitives (Badge, Card, StatCard, Kbd) - AIWorkflowSection.tsx ← AI workflow and smart utils showcase + ui/ ← composable UI primitives (Dialog, Sheet, Tabs, Alert, EmptyState, Table, Progress, Switch, Badge, Card, StatCard, Skeleton, Tooltip, Kbd, Icon) + examples/ ← reusable pattern components (MetricsDashboard, SettingsPanel, ServiceTable, DeploymentModal) + AIWorkflowSection.tsx ← interactive pattern switcher & showcase + IconMatrix.tsx ← interactive Lucide icon explorer & search Topbar.tsx ← owns the theme toggle (.dark & data-theme) Hero.tsx, CodeSample.tsx, RightRail.tsx, ButtonMatrix.tsx, Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx, MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx, Icon.tsx data/ library.ts ← demo content and color token data for the showcase - ai-rules.ts ← AI copilot prompt and quick implementation recipes scripts/check-tokens.mjs ← token contract guard (part of npm run check) ``` diff --git a/package-lock.json b/package-lock.json index 02ff505..1bce8d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "design-system", "version": "0.0.0", "dependencies": { + "lucide-react": "^1.45.0", "react": "^19.2.8", "react-dom": "^19.2.8" }, @@ -1041,6 +1042,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lucide-react": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.45.0.tgz", + "integrity": "sha512-yH1ubCAduho9UR7oJhRXIQXogksRILBiTuZC4/bQIGeB9JOkxMlSuEHyyZpo1Z3S0yWJO2KTSUZbjiNvVxeOUw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.19", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", diff --git a/package.json b/package.json index 8be1384..fd64d16 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "preview": "vite preview" }, "dependencies": { + "lucide-react": "^1.45.0", "react": "^19.2.8", "react-dom": "^19.2.8" }, diff --git a/src/App.css b/src/App.css index adc87ad..99d6aa4 100644 --- a/src/App.css +++ b/src/App.css @@ -13,8 +13,18 @@ /* ── Shell & topbar ─────────────────────────────────────────── */ .app-shell { min-height: 100vh; - background: var(--background); - background-image: radial-gradient(circle at 71% 1%, color-mix(in srgb, var(--primary) 9%, transparent), transparent 28rem); + background-color: var(--background); + position: relative; + isolation: isolate; +} + +.app-shell::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background-image: radial-gradient(ellipse 90% 60% at 70% -5%, color-mix(in srgb, var(--primary) 3.5%, transparent) 0%, transparent 100%); } .topbar { @@ -101,35 +111,45 @@ flex: 1; margin-left: auto; margin-right: auto; - gap: 10px; + gap: var(--space-3); color: var(--muted-foreground); background: var(--card); border: 1px solid var(--border); - border-radius: 7px; - padding: var(--space-2) 11px; + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-3); + cursor: pointer; + text-align: left; + font-family: inherit; + font-size: var(--text-sm); + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; } -.top-search:focus-within { +.top-search:hover { border-color: var(--ring); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 12%, transparent); + background: color-mix(in srgb, var(--accent) 35%, var(--card)); } -.top-search input { - background: none; - border: 0; +.top-search:focus-visible { outline: 0; - color: var(--foreground); - min-width: 0; + border-color: var(--ring); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); +} + +.top-search-placeholder { flex: 1; + color: var(--muted-foreground); + user-select: none; font-size: var(--text-sm); } .top-search kbd { - font: var(--text-3xs) var(--font-mono); + font-family: var(--font-mono); + font-size: var(--text-3xs); color: var(--muted-foreground); border: 1px solid var(--border); - border-radius: 4px; - padding: var(--space-1) var(--space-1); + border-radius: var(--radius-sm); + padding: var(--space-1) var(--space-2); + background: var(--secondary); } .top-links { @@ -300,17 +320,6 @@ box-shadow: inset 2px 0 var(--sidebar-primary); } -.nav-item.active::before { - content: ""; - position: absolute; - right: var(--space-5); - width: 5px; - height: 5px; - border-radius: 50%; - background: var(--sidebar-primary); - box-shadow: 0 0 10px color-mix(in srgb, var(--sidebar-primary) 80%, transparent); -} - .nav-glyph { width: 13px; color: var(--muted-foreground); @@ -426,24 +435,49 @@ display: inline-flex; align-items: center; gap: var(--space-2); - transition: 0.2s; + transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.08s ease, filter 0.12s ease; white-space: nowrap; - font-weight: 500; + font-weight: 600; position: relative; overflow: hidden; + cursor: pointer; + user-select: none; + outline: none; +} + +.button:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; +} + +.button:active { + transform: translateY(1px); } .button.primary { background: var(--primary); color: var(--primary-foreground); - font-weight: 700; - box-shadow: 0 4px 18px color-mix(in srgb, var(--primary) 28%, transparent); + font-weight: 600; + border-color: transparent; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } -.button.primary:hover { - filter: brightness(1.1); - transform: translateY(-1px); - box-shadow: 0 6px 22px color-mix(in srgb, var(--primary) 40%, transparent); +.button.primary:hover, +.button.primary.hover-state { + filter: brightness(1.06); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28); +} + +.button.primary:active { + filter: brightness(0.93); + transform: translateY(1px); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.button.primary:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 35%, transparent); } .button.secondary { @@ -453,20 +487,45 @@ } .button.secondary:hover, -.button.hover-state { +.button.secondary.hover-state { background: color-mix(in srgb, var(--foreground) 14%, var(--card)); border-color: var(--ring); } -.button.danger { - background: color-mix(in srgb, var(--destructive) 14%, transparent); - color: var(--destructive); - border: 1px solid color-mix(in srgb, var(--destructive) 32%, transparent); +.button.secondary:active { + background: color-mix(in srgb, var(--foreground) 18%, var(--card)); + transform: translateY(1px); } -.button.danger:hover { +.button.secondary:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--foreground) 15%, transparent); +} + +.button.danger { background: var(--destructive); color: var(--destructive-foreground); + border: 1px solid transparent; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.button.danger:hover, +.button.danger.hover-state { + filter: brightness(1.08); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28); +} + +.button.danger:active { + filter: brightness(0.92); + transform: translateY(1px); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.button.danger:focus-visible { + outline: 2px solid var(--destructive); + outline-offset: 2px; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--destructive) 35%, transparent); } .button.ghost { @@ -474,11 +533,22 @@ color: var(--muted-foreground); } -.button.ghost:hover { +.button.ghost:hover, +.button.ghost.hover-state { color: var(--foreground); background: var(--accent); } +.button.ghost:active { + background: color-mix(in srgb, var(--accent) 80%, var(--foreground)); + transform: translateY(1px); +} + +.button.ghost:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; +} + .hero-orbit { width: 280px; height: 225px; @@ -568,6 +638,7 @@ .section-block { padding: var(--space-9) 0; border-top: 1px solid var(--border); + scroll-margin-top: 80px; } .section-heading { @@ -608,7 +679,7 @@ /* ── Button showcase ────────────────────────────────────────── */ .tabs { display: flex; - gap: var(--space-5); + gap: var(--space-6); border-bottom: 1px solid var(--border); margin-top: var(--space-7); } @@ -617,14 +688,32 @@ border: 0; background: none; color: var(--muted-foreground); - font-size: var(--text-xs); - padding: 0 0 var(--space-3); - border-bottom: 1px solid transparent; + font-family: var(--font-sans); + font-size: var(--text-sm); + font-weight: 500; + line-height: 1.4; + padding: var(--space-2) 0 var(--space-3); + border-bottom: 2px solid transparent; + display: inline-flex; + align-items: center; + gap: var(--space-2); + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.tab:hover { + color: var(--foreground); } .tab.active { color: var(--foreground); border-color: var(--primary); + font-weight: 600; +} + +.tab svg { + display: block; + flex-shrink: 0; } .showcase { @@ -908,34 +997,71 @@ } .segmented { - display: flex; - gap: var(--space-1); + display: inline-flex; + align-items: center; + gap: 4px; + background: var(--secondary); + border: 1px solid var(--border); + padding: 4px; + border-radius: var(--radius-lg); } -.segmented button, -.copy-button { - border: 0; - background: none; +.segmented button { + border: 1px solid transparent; + background: transparent; color: var(--muted-foreground); - font: var(--text-3xs) var(--font-mono); - padding: var(--space-1) 7px; - border-radius: 4px; + font-family: var(--font-sans); + font-size: var(--text-xs); + font-weight: 500; + line-height: 1.4; + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + display: inline-flex; + align-items: center; + gap: var(--space-2); + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.segmented button:hover { + color: var(--foreground); } .segmented button.selected { - background: color-mix(in srgb, var(--primary) 14%, var(--secondary)); + background: var(--card); + color: var(--foreground); + border-color: color-mix(in srgb, var(--border) 60%, transparent); + font-weight: 600; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); +} + +.segmented button.selected svg { color: var(--primary); - font-weight: 500; +} + +.segmented button svg { + display: block; + flex-shrink: 0; } .copy-button { - display: flex; - gap: var(--space-1); + border: 1px solid var(--border); + background: var(--secondary); + color: var(--muted-foreground); + font: var(--text-3xs) var(--font-mono); + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-md); + display: inline-flex; align-items: center; + gap: var(--space-2); + cursor: pointer; + transition: 0.15s ease; } .copy-button:hover { color: var(--foreground); + border-color: var(--ring); } .code-card pre { @@ -1475,8 +1601,388 @@ footer { transition: border-color 0.25s ease, box-shadow 0.25s ease; } -.top-search:hover { +/* ── Icon Matrix & Explorer ─────────────────────────────────── */ +.icon-matrix-container { + display: flex; + flex-direction: column; + gap: var(--space-5); +} + +.icon-matrix-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + flex-wrap: wrap; +} + +.icon-search-bar { + display: flex; + align-items: center; + gap: var(--space-3); + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--space-2) var(--space-4); + flex: 1; + min-width: 240px; + color: var(--muted-foreground); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.icon-search-bar:focus-within { border-color: var(--ring); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 12%, transparent); +} + +.icon-search-input { + background: none; + border: 0; + outline: 0; + color: var(--foreground); + font-size: var(--text-sm); + width: 100%; +} + +.icon-clear-btn { + background: none; + border: 0; + color: var(--muted-foreground); + cursor: pointer; + padding: 2px; + display: grid; + place-items: center; + border-radius: var(--radius-sm); +} + +.icon-clear-btn:hover { + color: var(--foreground); +} + +.icon-size-picker { + display: flex; + align-items: center; + gap: var(--space-2); + background: var(--secondary); + padding: 3px 4px; + border-radius: var(--radius-md); + border: 1px solid var(--border); +} + +.icon-size-btn { + border: 0; + background: none; + color: var(--muted-foreground); + font: var(--text-3xs) var(--font-mono); + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + cursor: pointer; + transition: 0.15s ease; +} + +.icon-size-btn:hover { + color: var(--foreground); +} + +.icon-size-btn.active { + background: var(--card); + color: var(--foreground); + box-shadow: 0 1px 4px color-mix(in srgb, var(--foreground) 10%, transparent); +} + +.icon-category-tabs { + display: flex; + gap: var(--space-2); + overflow-x: auto; + padding-bottom: 2px; +} + +.icon-cat-btn { + border: 1px solid var(--border); + background: var(--card); + color: var(--muted-foreground); + font-family: var(--font-sans); + font-size: var(--text-sm); + font-weight: 500; + line-height: 1.4; + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: var(--space-2); + transition: all 0.15s ease; + white-space: nowrap; +} + +.icon-cat-btn svg { + display: block; + flex-shrink: 0; +} + +.icon-cat-btn:hover { + color: var(--foreground); + border-color: var(--ring); +} + +.icon-cat-btn.active { + background: color-mix(in srgb, var(--primary) 10%, var(--card)); + border-color: var(--primary); + color: var(--primary); +} + +.icon-cat-count { + font: var(--text-3xs) var(--font-mono); + opacity: 0.8; +} + +.icon-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(115px, 1fr)); + gap: var(--space-3); +} + +.icon-tile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-4) var(--space-2); + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + color: var(--foreground); + position: relative; + text-align: center; +} + +.icon-tile:hover { + border-color: var(--ring); + transform: translateY(-2px); + box-shadow: 0 8px 20px color-mix(in srgb, var(--primary) 10%, transparent); + background: color-mix(in srgb, var(--foreground) 3%, var(--card)); +} + +.icon-tile.copied { + border-color: var(--primary); + background: color-mix(in srgb, var(--primary) 12%, var(--card)); +} + +.icon-tile-glyph { + display: grid; + place-items: center; + height: 32px; + color: var(--foreground); + transition: color 0.15s ease; +} + +.icon-tile:hover .icon-tile-glyph { + color: var(--primary); +} + +.icon-tile-name { + font: var(--text-3xs) var(--font-mono); + color: var(--muted-foreground); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.icon-tile-action { + font: var(--text-3xs) var(--font-mono); + color: var(--primary); + min-height: 16px; +} + +.icon-matrix-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: var(--space-3); + border-top: 1px solid var(--border); +} + +.icon-footer-pill { + display: flex; + align-items: center; + gap: var(--space-3); + font-size: var(--text-xs); + color: var(--muted-foreground); +} + +.icon-footer-pill code { + font-family: var(--font-mono); + color: var(--primary); +} + +/* ── Command Palette & Topbar Search ────────────────────────── */ +.search-palette-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); + z-index: 100; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 12vh var(--space-4) var(--space-4); + animation: dialog-fade-in 0.15s ease-out; +} + +.search-palette-modal { + width: 100%; + max-width: 580px; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45); + overflow: hidden; + display: flex; + flex-direction: column; + animation: modal-scale-in 0.18s cubic-bezier(0.16, 1, 0.3, 1); +} + +.search-palette-head { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-4) var(--space-5); + border-bottom: 1px solid var(--border); + background: var(--card); +} + +.search-palette-icon { + color: var(--muted-foreground); + flex-shrink: 0; +} + +.search-palette-input { + flex: 1; + background: none; + border: 0; + outline: none; + font-family: var(--font-sans); + font-size: var(--text-md); + color: var(--foreground); + min-width: 0; +} + +.search-palette-input::placeholder { + color: var(--muted-foreground); +} + +.search-palette-body { + max-height: 380px; + overflow-y: auto; + padding: var(--space-2); +} + +.search-palette-results { + display: flex; + flex-direction: column; + gap: 2px; +} + +.search-palette-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-lg); + cursor: pointer; + transition: background-color 0.1s ease; +} + +.search-palette-item:hover, +.search-palette-item.selected { + background: var(--secondary); +} + +.search-palette-item.selected .search-item-glyph { + color: var(--primary); + background: color-mix(in srgb, var(--primary) 12%, transparent); +} + +.search-item-glyph { + width: 32px; + height: 32px; + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--foreground) 6%, transparent); + color: var(--muted-foreground); + display: grid; + place-items: center; + flex-shrink: 0; + transition: all 0.15s ease; +} + +.search-item-content { + flex: 1; + min-width: 0; +} + +.search-item-title-row { + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: 2px; +} + +.search-item-title { + font-family: var(--font-sans); + font-size: var(--text-sm); + font-weight: 600; + color: var(--foreground); +} + +.search-item-desc { + margin: 0; + font-size: var(--text-xs); + color: var(--muted-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-item-action { + flex-shrink: 0; +} + +.search-palette-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-8) var(--space-4); + text-align: center; + color: var(--muted-foreground); +} + +.search-palette-empty p { + margin: var(--space-2) 0 var(--space-1); + font-size: var(--text-sm); + font-weight: 500; + color: var(--foreground); +} + +.search-palette-empty span { + font-size: var(--text-xs); + color: var(--muted-foreground); +} + +.search-palette-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-4); + padding: var(--space-2) var(--space-4); + border-top: 1px solid var(--border); + background: color-mix(in srgb, var(--secondary) 40%, var(--card)); + font-size: var(--text-3xs); + color: var(--muted-foreground); } @media (prefers-reduced-motion: reduce) { diff --git a/src/App.tsx b/src/App.tsx index 47e2053..9a6088c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import './App.css' import { Topbar } from './components/Topbar' import { Sidebar } from './components/Sidebar' @@ -9,9 +9,12 @@ import { FormField, TextInput } from './components/FormField' import { CodeSample } from './components/CodeSample' import { MarkdownRenderer } from './components/MarkdownRenderer' import { CodeEditor } from './components/CodeEditor' -import { AIWorkflowSection } from './components/AIWorkflowSection' +import { AIWorkflowSection, type PatternId } from './components/AIWorkflowSection' +import { IconMatrix } from './components/IconMatrix' +import { TypographySection } from './components/TypographySection' import { Section } from './components/Section' import { RightRail } from './components/RightRail' +import { SearchPalette, type SearchItem } from './components/SearchPalette' import { Icon } from './components/Icon' import { buttonExamples, buttonTabs, colorTokens, navigationGroups, pageSections } from './data/library' @@ -19,23 +22,62 @@ const INSTALL_CMD = 'pnpm add @northstar/ui @northstar/tokens' export default function App() { const [active, setActive] = useState('Buttons') - const [query, setQuery] = useState('') + const [searchOpen, setSearchOpen] = useState(false) const [buttonTab, setButtonTab] = useState('Preview') const [visible, setVisible] = useState(false) const [menu, setMenu] = useState(false) + const [pattern, setPattern] = useState('metrics') + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault() + setSearchOpen(prev => !prev) + } + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, []) + + const handleNavigate = (item: { label: string; id?: string }) => { + setActive(item.label) + setMenu(false) + + // Synchronize active pattern when clicking pattern items + if (item.label === 'Metrics Dashboard') setPattern('metrics') + else if (item.label === 'Settings Panel') setPattern('settings') + else if (item.label === 'Service Table') setPattern('table') + else if (item.label === 'Deployment Flow') setPattern('deploy') + + const targetId = item.id || item.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + const el = document.getElementById(targetId) + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }) + window.history.replaceState(null, '', `#${targetId}`) + } + } + + const handleSearchSelect = (item: SearchItem) => { + if (item.patternId) { + setPattern(item.patternId) + } + handleNavigate({ label: item.title, id: item.targetId }) + } return (
- setMenu(!menu)} /> + setSearchOpen(true)} onMenuToggle={() => setMenu(!menu)} /> + setSearchOpen(false)} + onSelect={handleSearchSelect} + />
{ - setActive(item) - setMenu(false) - }} + onSelect={handleNavigate} />
@@ -50,13 +92,13 @@ export default function App() {
lab() colors · cn() · primitives} + id="application-patterns" + index="01 / PATTERNS & EXAMPLES" + title="Reusable Application Patterns" + description="Production-grade, fully functional composable components ready to drop into any project." + meta={4 Reusable Components} > - +
+
lucide-react · 1,400+ icons} + > + +
+
+
Type scale · 9→70px} + > + +
+
@@ -121,7 +182,7 @@ export default function App() {
@@ -130,7 +191,7 @@ export default function App() {
Markdown · styled} @@ -140,7 +201,7 @@ export default function App() {
codemirror · dark} diff --git a/src/components/AIWorkflowSection.tsx b/src/components/AIWorkflowSection.tsx index 7a5336f..37ceb16 100644 --- a/src/components/AIWorkflowSection.tsx +++ b/src/components/AIWorkflowSection.tsx @@ -1,181 +1,86 @@ import { useState } from 'react' -import { StatCard } from './ui/StatCard' +import { MetricsDashboard } from './examples/MetricsDashboard' +import { SettingsPanel } from './examples/SettingsPanel' +import { ServiceTable } from './examples/ServiceTable' +import { DeploymentModal } from './examples/DeploymentModal' +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from './ui/Card' import { Badge } from './ui/Badge' -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/Card' -import { Kbd } from './ui/Kbd' import { Icon } from './Icon' -import { AI_SYSTEM_PROMPT, AI_QUICK_RECIPES } from '../data/ai-rules' -export function AIWorkflowSection() { - const [copied, setCopied] = useState(false) - const [activeRecipe, setActiveRecipe] = useState(0) +const PATTERNS = [ + { id: 'metrics', label: 'Metrics Dashboard', icon: 'sparkles' }, + { id: 'settings', label: 'Settings & Safeguards', icon: 'settings' }, + { id: 'table', label: 'Service Monitor Table', icon: 'layers' }, + { id: 'deploy', label: 'Deployment Flow', icon: 'terminal' }, +] as const - const copyPrompt = () => { - navigator.clipboard?.writeText(AI_SYSTEM_PROMPT) - setCopied(true) - setTimeout(() => setCopied(false), 2000) +export type PatternId = (typeof PATTERNS)[number]['id'] + +type AIWorkflowSectionProps = { + activePattern?: PatternId + onPatternChange?: (pattern: PatternId) => void +} + +export function AIWorkflowSection({ + activePattern: controlledPattern, + onPatternChange, +}: AIWorkflowSectionProps = {}) { + const [internalPattern, setInternalPattern] = useState('metrics') + const activePattern = controlledPattern ?? internalPattern + + const handleSelect = (id: PatternId) => { + setInternalPattern(id) + onPatternChange?.(id) } return (
- {/* ── Metric Highlights using StatCard Primitives ── */} -
- } - /> - } - /> - } - /> -
- - {/* ── Dot Canvas & Composable UI Showcase ── */} -
-
-
-
- AI Greenfield - lab() Color 4 - Uniform Architecture -
-

- High-Velocity Autonomous Building Canvas -

-

- Rendered on the user-defined --dot-color canvas with composable primitives. -

-
- - + {/* Pattern Selection Header */} +
+
+ {PATTERNS.map(p => ( + + ))}
- {/* ── Component Primitives Gallery ── */} -
- - -
- Autonomous Agent Run - Active -
- - Executing token validation and layout synthesis across 4 workers. - -
- -
-
- Token Verification - PASS (0 errors) -
-
- Color Engine - CSS Color 4 lab() -
-
- Theme Switching - .dark & [data-theme] -
-
-
- - - Press ⌘ + K for palette - - - -
- - - -
- Smart Primitives & Status - Composable -
- - Zero-drift badges, cards, and keyboard indicators ready for prompt generation. - -
- -
-
- Badge Variants: -
-
- Primary - Positive - Secondary - Destructive - Outline -
-
- Shortcuts & Flags: -
-
- Shift + D - toggle theme -
-
-
- - - ✓ Strict token contract compliant - - -
+
+ Interactive Component + Composable
- {/* ── AI Quick Prompt Recipes ── */} -
-
-
- {AI_QUICK_RECIPES.map((recipe, idx) => ( - - ))} + {/* Render Active Real Example Component */} +
+ {activePattern === 'metrics' && } + + {activePattern === 'settings' && } + + {activePattern === 'table' && } + + {activePattern === 'deploy' && ( +
+ + + Canary Deployment Orchestrator + + A complete interactive modal flow demonstrating Dialog, Alert, Switch, and Progress components. + + + +
+ +
+
+
- -
-
-          {AI_QUICK_RECIPES[activeRecipe].code}
-        
+ )}
) diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index cd5ba7a..760d2e2 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,33 +1,138 @@ -type IconProps = { name: string; size?: number } +import type { ComponentType, SVGProps } from 'react' +import { + Search, + Sun, + Moon, + Copy, + Check, + Eye, + ArrowRight, + ArrowLeft, + Menu, + Layers, + Sparkles, + Terminal, + AlertTriangle, + AlertCircle, + Info, + CheckCircle2, + X, + Plus, + Minus, + Trash2, + ExternalLink, + ChevronDown, + ChevronRight, + ChevronLeft, + ChevronUp, + Filter, + SlidersHorizontal, + Code2, + Cpu, + Database, + User, + Shield, + Folder, + FileText, + Calendar, + Clock, + RefreshCw, + Download, + Upload, + Settings, + HelpCircle, + type LucideIcon, + type LucideProps, +} from 'lucide-react' -const paths: Record = { - search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm6-2 4 4', - sun: 'M12 3v2m0 14v2M4 12h2m12 0h2M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z', - moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z', - copy: 'M8 8h10v12H8zM6 16H4V4h12v2', - check: 'm5 12 4 4L19 6', - eye: 'M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Zm10 3a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', - arrow: 'M5 12h14m-6-6 6 6-6 6', - menu: 'M4 6h16M4 12h16M4 18h16', - layers: 'm12 3 9 5-9 5-9-5 9-5Zm-9 9 9 5 9-5M3 16l9 5 9-5', - sparkle: 'm12 3 2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3Z', - terminal: 'm4 17 6-6-6-6m8 14h8', +export type { LucideIcon, LucideProps } + +const iconMap: Record = { + // Northstar legacy shorthands + search: Search, + sun: Sun, + moon: Moon, + copy: Copy, + check: Check, + eye: Eye, + arrow: ArrowRight, + 'arrow-right': ArrowRight, + 'arrow-left': ArrowLeft, + menu: Menu, + layers: Layers, + sparkle: Sparkles, + sparkles: Sparkles, + terminal: Terminal, + + // Common UI primitives + close: X, + x: X, + plus: Plus, + minus: Minus, + trash: Trash2, + 'trash-2': Trash2, + settings: Settings, + filter: Filter, + sliders: SlidersHorizontal, + refresh: RefreshCw, + download: Download, + upload: Upload, + external: ExternalLink, + 'external-link': ExternalLink, + info: Info, + warning: AlertTriangle, + 'alert-triangle': AlertTriangle, + alert: AlertCircle, + 'alert-circle': AlertCircle, + 'check-circle': CheckCircle2, + help: HelpCircle, + 'chevron-down': ChevronDown, + 'chevron-right': ChevronRight, + 'chevron-left': ChevronLeft, + 'chevron-up': ChevronUp, + code: Code2, + cpu: Cpu, + database: Database, + user: User, + shield: Shield, + folder: Folder, + file: FileText, + calendar: Calendar, + clock: Clock, } -export function Icon({ name, size = 18 }: IconProps) { +export type IconName = keyof typeof iconMap | string + +export type IconProps = Omit, 'ref'> & { + name?: IconName + icon?: LucideIcon | ComponentType<{ size?: number | string; strokeWidth?: number; className?: string }> + size?: number | string + strokeWidth?: number + className?: string +} + +/** + * Universal Icon component backed by lucide-react. + * Accepts either a shorthand name (e.g. name="sparkle", name="check") + * or a direct Lucide icon component (e.g. icon={Sparkles}). + */ +export function Icon({ + name, + icon: IconComponent, + size = 18, + strokeWidth = 1.8, + className, + ...props +}: IconProps) { + const Component = IconComponent || (name ? iconMap[name.toLowerCase()] : undefined) || Layers + return ( - + {...props} + /> ) } diff --git a/src/components/IconMatrix.tsx b/src/components/IconMatrix.tsx new file mode 100644 index 0000000..c27daa0 --- /dev/null +++ b/src/components/IconMatrix.tsx @@ -0,0 +1,233 @@ +import { useState, useMemo } from 'react' +import { + Search, + Sun, + Moon, + Copy, + Check, + Eye, + ArrowRight, + ArrowLeft, + Menu, + Layers, + Sparkles, + Terminal, + AlertTriangle, + AlertCircle, + Info, + CheckCircle2, + X, + Plus, + Minus, + Trash2, + ExternalLink, + ChevronDown, + ChevronRight, + ChevronLeft, + ChevronUp, + Filter, + SlidersHorizontal, + Code2, + Cpu, + Database, + User, + Shield, + Folder, + FileText, + Calendar, + Clock, + RefreshCw, + Download, + Upload, + Settings, + HelpCircle, + type LucideIcon, +} from 'lucide-react' +import { Icon } from './Icon' +import { Kbd } from './ui/Kbd' +import { Badge } from './ui/Badge' + +type IconItem = { + name: string + pascalName: string + category: 'Navigation' | 'Interface' | 'System' | 'Status' + component: LucideIcon +} + +const ICONS: IconItem[] = [ + // Navigation + { name: 'arrow-right', pascalName: 'ArrowRight', category: 'Navigation', component: ArrowRight }, + { name: 'arrow-left', pascalName: 'ArrowLeft', category: 'Navigation', component: ArrowLeft }, + { name: 'chevron-right', pascalName: 'ChevronRight', category: 'Navigation', component: ChevronRight }, + { name: 'chevron-left', pascalName: 'ChevronLeft', category: 'Navigation', component: ChevronLeft }, + { name: 'chevron-down', pascalName: 'ChevronDown', category: 'Navigation', component: ChevronDown }, + { name: 'chevron-up', pascalName: 'ChevronUp', category: 'Navigation', component: ChevronUp }, + { name: 'external-link', pascalName: 'ExternalLink', category: 'Navigation', component: ExternalLink }, + { name: 'menu', pascalName: 'Menu', category: 'Navigation', component: Menu }, + + // System & AI + { name: 'sparkles', pascalName: 'Sparkles', category: 'System', component: Sparkles }, + { name: 'terminal', pascalName: 'Terminal', category: 'System', component: Terminal }, + { name: 'code', pascalName: 'Code2', category: 'System', component: Code2 }, + { name: 'cpu', pascalName: 'Cpu', category: 'System', component: Cpu }, + { name: 'layers', pascalName: 'Layers', category: 'System', component: Layers }, + { name: 'sun', pascalName: 'Sun', category: 'System', component: Sun }, + { name: 'moon', pascalName: 'Moon', category: 'System', component: Moon }, + { name: 'database', pascalName: 'Database', category: 'System', component: Database }, + + // Interface & Actions + { name: 'search', pascalName: 'Search', category: 'Interface', component: Search }, + { name: 'copy', pascalName: 'Copy', category: 'Interface', component: Copy }, + { name: 'settings', pascalName: 'Settings', category: 'Interface', component: Settings }, + { name: 'sliders', pascalName: 'SlidersHorizontal', category: 'Interface', component: SlidersHorizontal }, + { name: 'filter', pascalName: 'Filter', category: 'Interface', component: Filter }, + { name: 'plus', pascalName: 'Plus', category: 'Interface', component: Plus }, + { name: 'minus', pascalName: 'Minus', category: 'Interface', component: Minus }, + { name: 'trash', pascalName: 'Trash2', category: 'Interface', component: Trash2 }, + { name: 'refresh', pascalName: 'RefreshCw', category: 'Interface', component: RefreshCw }, + { name: 'download', pascalName: 'Download', category: 'Interface', component: Download }, + { name: 'upload', pascalName: 'Upload', category: 'Interface', component: Upload }, + { name: 'folder', pascalName: 'Folder', category: 'Interface', component: Folder }, + { name: 'file', pascalName: 'FileText', category: 'Interface', component: FileText }, + { name: 'user', pascalName: 'User', category: 'Interface', component: User }, + { name: 'calendar', pascalName: 'Calendar', category: 'Interface', component: Calendar }, + { name: 'clock', pascalName: 'Clock', category: 'Interface', component: Clock }, + + // Status & Feedback + { name: 'check', pascalName: 'Check', category: 'Status', component: Check }, + { name: 'check-circle', pascalName: 'CheckCircle2', category: 'Status', component: CheckCircle2 }, + { name: 'warning', pascalName: 'AlertTriangle', category: 'Status', component: AlertTriangle }, + { name: 'alert', pascalName: 'AlertCircle', category: 'Status', component: AlertCircle }, + { name: 'info', pascalName: 'Info', category: 'Status', component: Info }, + { name: 'close', pascalName: 'X', category: 'Status', component: X }, + { name: 'shield', pascalName: 'Shield', category: 'Status', component: Shield }, + { name: 'help', pascalName: 'HelpCircle', category: 'Status', component: HelpCircle }, + { name: 'eye', pascalName: 'Eye', category: 'Status', component: Eye }, +] + +const CATEGORIES = ['All', 'Navigation', 'Interface', 'System', 'Status'] as const +type Category = (typeof CATEGORIES)[number] + +export function IconMatrix() { + const [activeCategory, setActiveCategory] = useState('All') + const [search, setSearch] = useState('') + const [size, setSize] = useState(20) + const [copiedName, setCopiedName] = useState(null) + + const filteredIcons = useMemo(() => { + return ICONS.filter(icon => { + const matchesCategory = activeCategory === 'All' || icon.category === activeCategory + const matchesSearch = + icon.name.toLowerCase().includes(search.toLowerCase()) || + icon.pascalName.toLowerCase().includes(search.toLowerCase()) + return matchesCategory && matchesSearch + }) + }, [activeCategory, search]) + + const copyUsage = (icon: IconItem) => { + const code = `<${icon.pascalName} size={${size}} />` + navigator.clipboard?.writeText(code) + setCopiedName(icon.name) + setTimeout(() => setCopiedName(null), 2000) + } + + return ( +
+ {/* Header controls */} +
+
+ + setSearch(e.target.value)} + className="icon-search-input" + /> + {search && ( + + )} +
+ +
+ Size: + {[16, 20, 24].map(s => ( + + ))} +
+
+ + {/* Category tabs */} +
+ {CATEGORIES.map(cat => ( + + ))} +
+ + {/* Grid of icons */} +
+ {filteredIcons.map(icon => { + const isCopied = copiedName === icon.name + return ( + + ) + })} +
+ + {filteredIcons.length === 0 && ( +
+
+ +
+

No icons found

+

+ No icon matches "{search}". You can use any of Lucide's 1,400+ icons directly via{' '} + lucide-react. +

+
+ )} + + {/* Footer usage tip */} +
+
+ lucide-react + Over 1,400+ SVG icons available. Fully tree-shaken and styled via currentColor. +
+
+
+ ) +} diff --git a/src/components/SearchPalette.tsx b/src/components/SearchPalette.tsx new file mode 100644 index 0000000..b90ea3c --- /dev/null +++ b/src/components/SearchPalette.tsx @@ -0,0 +1,271 @@ +import { useState, useEffect, useMemo, useRef } from 'react' +import { Icon } from './Icon' +import { Badge } from './ui/Badge' +import { Kbd } from './ui/Kbd' +import type { PatternId } from './AIWorkflowSection' + +export type SearchItem = { + id: string + title: string + description: string + category: 'Patterns' | 'Components' | 'Foundations' | 'Documentation' + icon: string + targetId: string + patternId?: PatternId +} + +const SEARCH_INDEX: SearchItem[] = [ + // Patterns + { + id: 'pat-metrics', + title: 'Metrics Dashboard', + description: 'Telemetry dashboard with StatCards, time range picker, and progress gauges', + category: 'Patterns', + icon: 'sparkles', + targetId: 'application-patterns', + patternId: 'metrics', + }, + { + id: 'pat-settings', + title: 'Settings & Safeguards', + description: 'Preferences panel with API keys, toggle switches, and alert banners', + category: 'Patterns', + icon: 'settings', + targetId: 'application-patterns', + patternId: 'settings', + }, + { + id: 'pat-table', + title: 'Service Monitor Table', + description: 'Real-time service health table with status filtering and search', + category: 'Patterns', + icon: 'layers', + targetId: 'application-patterns', + patternId: 'table', + }, + { + id: 'pat-deploy', + title: 'Canary Deployment Flow', + description: 'Interactive rollout modal with live progress simulation', + category: 'Patterns', + icon: 'terminal', + targetId: 'application-patterns', + patternId: 'deploy', + }, + + // Components + { + id: 'comp-buttons', + title: 'Buttons & Action States', + description: 'Primary, secondary, destructive, ghost variants with resting, hover, active and focus states', + category: 'Components', + icon: 'arrow', + targetId: 'buttons', + }, + { + id: 'comp-icons', + title: 'Lucide Icons Explorer', + description: 'Over 1,400+ tree-shaken SVG icons with search, category filtering and 1-click copy', + category: 'Components', + icon: 'sparkles', + targetId: 'icons', + }, + { + id: 'comp-forms', + title: 'Form Controls & Inputs', + description: 'Accessible text inputs, secret key revealers, and toggle switches', + category: 'Components', + icon: 'eye', + targetId: 'form-controls', + }, + { + id: 'comp-code', + title: 'Composable UI Primitives', + description: 'Zero-drift Dialog, Sheet, Tabs, Table, Alert, Progress, Switch, and StatCard', + category: 'Components', + icon: 'code', + targetId: 'composable-by-default', + }, + + // Foundations + { + id: 'found-colors', + title: 'Color Tokens (CSS Color 4 lab())', + description: 'Pitch black dark theme and pure white daylight light theme tokens', + category: 'Foundations', + icon: 'sun', + targetId: 'color-tokens', + }, + { + id: 'found-type', + title: 'Typography & Scale', + description: 'Modular rem-based 9px to 70px scale with Manrope and DM Mono families', + category: 'Foundations', + icon: 'file', + targetId: 'typography', + }, + + // Documentation + { + id: 'doc-md', + title: 'Markdown Editorial Layer', + description: 'Changelogs, release notes, and documentation typography', + category: 'Documentation', + icon: 'file', + targetId: 'markdown-rendering', + }, + { + id: 'doc-cm', + title: 'CodeMirror Editor Theme', + description: 'Syntax-highlighted code editor preview with line numbers and token palette', + category: 'Documentation', + icon: 'terminal', + targetId: 'codemirror-theme', + }, +] + +type SearchPaletteProps = { + isOpen: boolean + onClose: () => void + onSelect: (item: SearchItem) => void + initialQuery?: string +} + +export function SearchPalette({ isOpen, onClose, onSelect, initialQuery = '' }: SearchPaletteProps) { + const [query, setQuery] = useState(initialQuery) + const [selectedIndex, setSelectedIndex] = useState(0) + const inputRef = useRef(null) + + useEffect(() => { + if (isOpen) { + inputRef.current?.focus() + } + }, [isOpen, initialQuery]) + + const results = useMemo(() => { + if (!query.trim()) return SEARCH_INDEX + const q = query.toLowerCase() + return SEARCH_INDEX.filter( + item => + item.title.toLowerCase().includes(q) || + item.description.toLowerCase().includes(q) || + item.category.toLowerCase().includes(q), + ) + }, [query]) + + // Keyboard navigation + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + onClose() + } else if (e.key === 'ArrowDown') { + e.preventDefault() + setSelectedIndex(prev => (prev + 1) % (results.length || 1)) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setSelectedIndex(prev => (prev - 1 + results.length) % (results.length || 1)) + } else if (e.key === 'Enter' && results[selectedIndex]) { + e.preventDefault() + onSelect(results[selectedIndex]) + onClose() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [isOpen, results, selectedIndex, onClose, onSelect]) + + if (!isOpen) return null + + return ( +
+
e.stopPropagation()}> + {/* Search Input Bar */} +
+ + { + setQuery(e.target.value) + setSelectedIndex(0) + }} + /> + {query && ( + + )} + ESC +
+ + {/* Results List */} +
+ {results.length > 0 ? ( +
+ {results.map((item, idx) => { + const isSelected = idx === selectedIndex + return ( +
{ + onSelect(item) + onClose() + }} + onMouseEnter={() => setSelectedIndex(idx)} + > +
+ +
+
+
+ {item.title} + {item.category} +
+

{item.description}

+
+ {isSelected && ( +
+ ↵ +
+ )} +
+ ) + })} +
+ ) : ( +
+ +

No results found for “{query}”

+ Try searching for “Buttons”, “Metrics”, “Tokens”, or “Icons”. +
+ )} +
+ + {/* Footer info */} +
+
+ ↑ + ↓ + to navigate +
+
+ ↵ + to select +
+
+ ESC + to close +
+
+
+
+ ) +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 5ec38e0..1869776 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,12 +1,12 @@ import { Icon } from './Icon' -export type NavigationItem = { label: string; count?: number } +export type NavigationItem = { label: string; count?: number; id?: string } export type NavigationGroup = { label: string; items: NavigationItem[] } type SidebarProps = { active: string groups: NavigationGroup[] - onSelect: (item: string) => void + onSelect: (item: NavigationItem) => void } export function Sidebar({ active, groups, onSelect }: SidebarProps) { @@ -34,7 +34,7 @@ export function Sidebar({ active, groups, onSelect }: SidebarProps) {
-
+
+