Template
fix
This commit is contained in:
1 parent
61df5e3dcc
commit
c0a94d3b6d
25 files changed
+2143
-348
No files matched your search
@@ -28,9 +28,10 @@ React 19 + Vite 8 + TypeScript design-system showcase. Dark-first, token-driven
|
|||||||
## Smart Utils & AI Workflow
|
## Smart Utils & AI Workflow
|
||||||
|
|
||||||
- **Class helper**: `src/utils/cn.ts` — zero-dependency classnames merging utility for conditional component styles.
|
- **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`).
|
- **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/` (`Badge.tsx`, `Card.tsx`, `StatCard.tsx`, `Kbd.tsx`).
|
- **Composable Primitives**: `src/components/ui/` (`Dialog`, `Sheet`, `Tabs`, `Alert`, `EmptyState`, `Table`, `Progress`, `Switch`, `Badge`, `Card`, `StatCard`, `Skeleton`, `Tooltip`, `Kbd`, `Icon`).
|
||||||
- **AI Agent System Prompt & Recipes**: `src/data/ai-rules.ts`.
|
- **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 (`<Icon name="..." />` or direct import).
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
@@ -44,15 +45,16 @@ src/
|
|||||||
utils/
|
utils/
|
||||||
cn.ts ← conditional classnames utility
|
cn.ts ← conditional classnames utility
|
||||||
components/
|
components/
|
||||||
ui/ ← composable UI primitives (Badge, Card, StatCard, Kbd)
|
ui/ ← composable UI primitives (Dialog, Sheet, Tabs, Alert, EmptyState, Table, Progress, Switch, Badge, Card, StatCard, Skeleton, Tooltip, Kbd, Icon)
|
||||||
AIWorkflowSection.tsx ← AI workflow and smart utils showcase
|
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)
|
Topbar.tsx ← owns the theme toggle (.dark & data-theme)
|
||||||
Hero.tsx, CodeSample.tsx, RightRail.tsx, ButtonMatrix.tsx,
|
Hero.tsx, CodeSample.tsx, RightRail.tsx, ButtonMatrix.tsx,
|
||||||
Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx,
|
Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx,
|
||||||
MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx, Icon.tsx
|
MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx, Icon.tsx
|
||||||
data/
|
data/
|
||||||
library.ts ← demo content and color token data for the showcase
|
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)
|
scripts/check-tokens.mjs ← token contract guard (part of npm run check)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Generated
+10
@@ -8,6 +8,7 @@
|
|||||||
"name": "design-system",
|
"name": "design-system",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lucide-react": "^1.45.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8"
|
||||||
},
|
},
|
||||||
@@ -1041,6 +1042,15 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"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": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.19",
|
"version": "3.3.19",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lucide-react": "^1.45.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8"
|
||||||
},
|
},
|
||||||
|
|||||||
+565
-59
@@ -13,8 +13,18 @@
|
|||||||
/* ── Shell & topbar ─────────────────────────────────────────── */
|
/* ── Shell & topbar ─────────────────────────────────────────── */
|
||||||
.app-shell {
|
.app-shell {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--background);
|
background-color: var(--background);
|
||||||
background-image: radial-gradient(circle at 71% 1%, color-mix(in srgb, var(--primary) 9%, transparent), transparent 28rem);
|
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 {
|
.topbar {
|
||||||
@@ -101,35 +111,45 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
gap: 10px;
|
gap: var(--space-3);
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 7px;
|
border-radius: var(--radius-md);
|
||||||
padding: var(--space-2) 11px;
|
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);
|
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 {
|
.top-search:focus-visible {
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
outline: 0;
|
||||||
color: var(--foreground);
|
border-color: var(--ring);
|
||||||
min-width: 0;
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-search-placeholder {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
user-select: none;
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-search kbd {
|
.top-search kbd {
|
||||||
font: var(--text-3xs) var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-3xs);
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: var(--radius-sm);
|
||||||
padding: var(--space-1) var(--space-1);
|
padding: var(--space-1) var(--space-2);
|
||||||
|
background: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-links {
|
.top-links {
|
||||||
@@ -300,17 +320,6 @@
|
|||||||
box-shadow: inset 2px 0 var(--sidebar-primary);
|
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 {
|
.nav-glyph {
|
||||||
width: 13px;
|
width: 13px;
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
@@ -426,24 +435,49 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
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;
|
white-space: nowrap;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
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 {
|
.button.primary {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: var(--primary-foreground);
|
color: var(--primary-foreground);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
box-shadow: 0 4px 18px color-mix(in srgb, var(--primary) 28%, transparent);
|
border-color: transparent;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.primary:hover {
|
.button.primary:hover,
|
||||||
filter: brightness(1.1);
|
.button.primary.hover-state {
|
||||||
transform: translateY(-1px);
|
filter: brightness(1.06);
|
||||||
box-shadow: 0 6px 22px color-mix(in srgb, var(--primary) 40%, transparent);
|
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 {
|
.button.secondary {
|
||||||
@@ -453,20 +487,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button.secondary:hover,
|
.button.secondary:hover,
|
||||||
.button.hover-state {
|
.button.secondary.hover-state {
|
||||||
background: color-mix(in srgb, var(--foreground) 14%, var(--card));
|
background: color-mix(in srgb, var(--foreground) 14%, var(--card));
|
||||||
border-color: var(--ring);
|
border-color: var(--ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.danger {
|
.button.secondary:active {
|
||||||
background: color-mix(in srgb, var(--destructive) 14%, transparent);
|
background: color-mix(in srgb, var(--foreground) 18%, var(--card));
|
||||||
color: var(--destructive);
|
transform: translateY(1px);
|
||||||
border: 1px solid color-mix(in srgb, var(--destructive) 32%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
background: var(--destructive);
|
||||||
color: var(--destructive-foreground);
|
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 {
|
.button.ghost {
|
||||||
@@ -474,11 +533,22 @@
|
|||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.ghost:hover {
|
.button.ghost:hover,
|
||||||
|
.button.ghost.hover-state {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background: var(--accent);
|
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 {
|
.hero-orbit {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
height: 225px;
|
height: 225px;
|
||||||
@@ -568,6 +638,7 @@
|
|||||||
.section-block {
|
.section-block {
|
||||||
padding: var(--space-9) 0;
|
padding: var(--space-9) 0;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
|
scroll-margin-top: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
@@ -608,7 +679,7 @@
|
|||||||
/* ── Button showcase ────────────────────────────────────────── */
|
/* ── Button showcase ────────────────────────────────────────── */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-5);
|
gap: var(--space-6);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
margin-top: var(--space-7);
|
margin-top: var(--space-7);
|
||||||
}
|
}
|
||||||
@@ -617,14 +688,32 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
font-size: var(--text-xs);
|
font-family: var(--font-sans);
|
||||||
padding: 0 0 var(--space-3);
|
font-size: var(--text-sm);
|
||||||
border-bottom: 1px solid transparent;
|
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 {
|
.tab.active {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showcase {
|
.showcase {
|
||||||
@@ -908,34 +997,71 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.segmented {
|
.segmented {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
gap: var(--space-1);
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented button,
|
.segmented button {
|
||||||
.copy-button {
|
border: 1px solid transparent;
|
||||||
border: 0;
|
background: transparent;
|
||||||
background: none;
|
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
font: var(--text-3xs) var(--font-mono);
|
font-family: var(--font-sans);
|
||||||
padding: var(--space-1) 7px;
|
font-size: var(--text-xs);
|
||||||
border-radius: 4px;
|
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 {
|
.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);
|
color: var(--primary);
|
||||||
font-weight: 500;
|
}
|
||||||
|
|
||||||
|
.segmented button svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button {
|
.copy-button {
|
||||||
display: flex;
|
border: 1px solid var(--border);
|
||||||
gap: var(--space-1);
|
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;
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button:hover {
|
.copy-button:hover {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
border-color: var(--ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-card pre {
|
.code-card pre {
|
||||||
@@ -1475,8 +1601,388 @@ footer {
|
|||||||
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
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);
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
+81
-20
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { Topbar } from './components/Topbar'
|
import { Topbar } from './components/Topbar'
|
||||||
import { Sidebar } from './components/Sidebar'
|
import { Sidebar } from './components/Sidebar'
|
||||||
@@ -9,9 +9,12 @@ import { FormField, TextInput } from './components/FormField'
|
|||||||
import { CodeSample } from './components/CodeSample'
|
import { CodeSample } from './components/CodeSample'
|
||||||
import { MarkdownRenderer } from './components/MarkdownRenderer'
|
import { MarkdownRenderer } from './components/MarkdownRenderer'
|
||||||
import { CodeEditor } from './components/CodeEditor'
|
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 { Section } from './components/Section'
|
||||||
import { RightRail } from './components/RightRail'
|
import { RightRail } from './components/RightRail'
|
||||||
|
import { SearchPalette, type SearchItem } from './components/SearchPalette'
|
||||||
import { Icon } from './components/Icon'
|
import { Icon } from './components/Icon'
|
||||||
import { buttonExamples, buttonTabs, colorTokens, navigationGroups, pageSections } from './data/library'
|
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() {
|
export default function App() {
|
||||||
const [active, setActive] = useState('Buttons')
|
const [active, setActive] = useState('Buttons')
|
||||||
const [query, setQuery] = useState('')
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
const [buttonTab, setButtonTab] = useState('Preview')
|
const [buttonTab, setButtonTab] = useState('Preview')
|
||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState(false)
|
||||||
const [menu, setMenu] = useState(false)
|
const [menu, setMenu] = useState(false)
|
||||||
|
const [pattern, setPattern] = useState<PatternId>('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 (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<Topbar query={query} onQueryChange={setQuery} onMenuToggle={() => setMenu(!menu)} />
|
<Topbar onOpenSearch={() => setSearchOpen(true)} onMenuToggle={() => setMenu(!menu)} />
|
||||||
|
<SearchPalette
|
||||||
|
isOpen={searchOpen}
|
||||||
|
onClose={() => setSearchOpen(false)}
|
||||||
|
onSelect={handleSearchSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={`body-layout ${menu ? 'menu-open' : ''}`}>
|
<div className={`body-layout ${menu ? 'menu-open' : ''}`}>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
active={active}
|
active={active}
|
||||||
groups={navigationGroups}
|
groups={navigationGroups}
|
||||||
onSelect={item => {
|
onSelect={handleNavigate}
|
||||||
setActive(item)
|
|
||||||
setMenu(false)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="main-content">
|
<main className="main-content">
|
||||||
@@ -50,13 +92,13 @@ export default function App() {
|
|||||||
<div className="content-grid">
|
<div className="content-grid">
|
||||||
<div className="content-main">
|
<div className="content-main">
|
||||||
<Section
|
<Section
|
||||||
id="ai-workflow-smart-utils"
|
id="application-patterns"
|
||||||
index="01 / AI WORKFLOW & SMART UTILS"
|
index="01 / PATTERNS & EXAMPLES"
|
||||||
title="Smart Utils & AI Iteration"
|
title="Reusable Application Patterns"
|
||||||
description="Engineered for AI copilot uniformity, zero visual drift, and instant composable prototyping."
|
description="Production-grade, fully functional composable components ready to drop into any project."
|
||||||
meta={<span className="api-pill">lab() colors · cn() · primitives</span>}
|
meta={<span className="api-pill">4 Reusable Components</span>}
|
||||||
>
|
>
|
||||||
<AIWorkflowSection />
|
<AIWorkflowSection activePattern={pattern} onPatternChange={setPattern} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
@@ -69,15 +111,24 @@ export default function App() {
|
|||||||
<ButtonMatrix
|
<ButtonMatrix
|
||||||
examples={buttonExamples}
|
examples={buttonExamples}
|
||||||
tabs={buttonTabs}
|
tabs={buttonTabs}
|
||||||
filter={query}
|
|
||||||
activeTab={buttonTab}
|
activeTab={buttonTab}
|
||||||
onTabChange={setButtonTab}
|
onTabChange={setButtonTab}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section
|
||||||
|
id="icons"
|
||||||
|
index="03 / COMPONENTS"
|
||||||
|
title="Icons (Lucide)"
|
||||||
|
description="Crisp, scalable SVG icons powered by Lucide. Tree-shakeable, stroke-matched, and token-aware."
|
||||||
|
meta={<span className="api-pill">lucide-react · 1,400+ icons</span>}
|
||||||
|
>
|
||||||
|
<IconMatrix />
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="color-tokens"
|
id="color-tokens"
|
||||||
index="03 / FOUNDATIONS"
|
index="04 / FOUNDATIONS"
|
||||||
title="Color tokens"
|
title="Color tokens"
|
||||||
description="Functional color roles in CSS Color 4 lab() keep every surface intentional."
|
description="Functional color roles in CSS Color 4 lab() keep every surface intentional."
|
||||||
meta={
|
meta={
|
||||||
@@ -89,9 +140,19 @@ export default function App() {
|
|||||||
<TokenGrid tokens={colorTokens} onCopy={copyInstall} />
|
<TokenGrid tokens={colorTokens} onCopy={copyInstall} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section
|
||||||
|
id="typography"
|
||||||
|
index="05 / FOUNDATIONS"
|
||||||
|
title="Typography"
|
||||||
|
description="Modular rem-based type scale with dual-family hierarchy (Manrope & DM Mono)."
|
||||||
|
meta={<span className="api-pill">Type scale · 9→70px</span>}
|
||||||
|
>
|
||||||
|
<TypographySection />
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="form-controls"
|
id="form-controls"
|
||||||
index="04 / PRIMITIVES"
|
index="06 / PRIMITIVES"
|
||||||
title="Form controls"
|
title="Form controls"
|
||||||
description="Inputs designed for focus, flow, and the occasional messy edge case."
|
description="Inputs designed for focus, flow, and the occasional messy edge case."
|
||||||
>
|
>
|
||||||
@@ -121,7 +182,7 @@ export default function App() {
|
|||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="composable-by-default"
|
id="composable-by-default"
|
||||||
index="05 / CODE"
|
index="07 / CODE"
|
||||||
title="Composable by default"
|
title="Composable by default"
|
||||||
description="Small APIs. Strong defaults. No surprises."
|
description="Small APIs. Strong defaults. No surprises."
|
||||||
>
|
>
|
||||||
@@ -130,7 +191,7 @@ export default function App() {
|
|||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="markdown-rendering"
|
id="markdown-rendering"
|
||||||
index="06 / CONTENT"
|
index="08 / CONTENT"
|
||||||
title="Markdown rendering"
|
title="Markdown rendering"
|
||||||
description="A readable editorial layer for changelogs, docs, and product notes."
|
description="A readable editorial layer for changelogs, docs, and product notes."
|
||||||
meta={<span className="api-pill">Markdown · styled</span>}
|
meta={<span className="api-pill">Markdown · styled</span>}
|
||||||
@@ -140,7 +201,7 @@ export default function App() {
|
|||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="codemirror-theme"
|
id="codemirror-theme"
|
||||||
index="07 / DEVELOPER TOOLING"
|
index="09 / DEVELOPER TOOLING"
|
||||||
title="CodeMirror theme"
|
title="CodeMirror theme"
|
||||||
description="A focused editor palette for examples, playgrounds, and embedded code."
|
description="A focused editor palette for examples, playgrounds, and embedded code."
|
||||||
meta={<span className="api-pill">codemirror · dark</span>}
|
meta={<span className="api-pill">codemirror · dark</span>}
|
||||||
|
|||||||
@@ -1,181 +1,86 @@
|
|||||||
import { useState } from 'react'
|
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 { Badge } from './ui/Badge'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/Card'
|
|
||||||
import { Kbd } from './ui/Kbd'
|
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { AI_SYSTEM_PROMPT, AI_QUICK_RECIPES } from '../data/ai-rules'
|
|
||||||
|
|
||||||
export function AIWorkflowSection() {
|
const PATTERNS = [
|
||||||
const [copied, setCopied] = useState(false)
|
{ id: 'metrics', label: 'Metrics Dashboard', icon: 'sparkles' },
|
||||||
const [activeRecipe, setActiveRecipe] = useState(0)
|
{ 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 = () => {
|
export type PatternId = (typeof PATTERNS)[number]['id']
|
||||||
navigator.clipboard?.writeText(AI_SYSTEM_PROMPT)
|
|
||||||
setCopied(true)
|
type AIWorkflowSectionProps = {
|
||||||
setTimeout(() => setCopied(false), 2000)
|
activePattern?: PatternId
|
||||||
|
onPatternChange?: (pattern: PatternId) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AIWorkflowSection({
|
||||||
|
activePattern: controlledPattern,
|
||||||
|
onPatternChange,
|
||||||
|
}: AIWorkflowSectionProps = {}) {
|
||||||
|
const [internalPattern, setInternalPattern] = useState<PatternId>('metrics')
|
||||||
|
const activePattern = controlledPattern ?? internalPattern
|
||||||
|
|
||||||
|
const handleSelect = (id: PatternId) => {
|
||||||
|
setInternalPattern(id)
|
||||||
|
onPatternChange?.(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack-lg">
|
<div className="stack-lg">
|
||||||
{/* ── Metric Highlights using StatCard Primitives ── */}
|
{/* Pattern Selection Header */}
|
||||||
<div className="grid-3">
|
<div className="flex-between" style={{ flexWrap: 'wrap', gap: 'var(--space-3)' }}>
|
||||||
<StatCard
|
<div className="segmented">
|
||||||
title="Iteration Velocity"
|
{PATTERNS.map(p => (
|
||||||
value="4.8x"
|
<button
|
||||||
change="+42%"
|
key={p.id}
|
||||||
trend="up"
|
className={activePattern === p.id ? 'selected' : ''}
|
||||||
caption="via zero-drift tokens"
|
onClick={() => handleSelect(p.id)}
|
||||||
icon={<Icon name="sparkle" size={16} />}
|
>
|
||||||
/>
|
<Icon name={p.icon} size={15} />
|
||||||
<StatCard
|
<span>{p.label}</span>
|
||||||
title="Token Uniformity"
|
</button>
|
||||||
value="100%"
|
))}
|
||||||
change="Guaranteed"
|
|
||||||
trend="up"
|
|
||||||
caption="check-tokens contract"
|
|
||||||
icon={<Icon name="check" size={16} />}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Smart Primitives"
|
|
||||||
value="8+ Utils"
|
|
||||||
change="Zero setup"
|
|
||||||
trend="neutral"
|
|
||||||
caption="cn, badges, cards, stats"
|
|
||||||
icon={<Icon name="terminal" size={16} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Dot Canvas & Composable UI Showcase ── */}
|
|
||||||
<div className="surface-card dot-canvas" style={{ padding: 'var(--space-6)' }}>
|
|
||||||
<div className="flex-between" style={{ marginBottom: 'var(--space-5)' }}>
|
|
||||||
<div>
|
|
||||||
<div className="row-sm" style={{ marginBottom: 'var(--space-1)' }}>
|
|
||||||
<Badge variant="primary">AI Greenfield</Badge>
|
|
||||||
<Badge variant="positive">lab() Color 4</Badge>
|
|
||||||
<Badge variant="outline">Uniform Architecture</Badge>
|
|
||||||
</div>
|
|
||||||
<h3 style={{ margin: 'var(--space-2) 0 var(--space-1)', fontSize: 'var(--text-xl)', letterSpacing: '-0.03em' }}>
|
|
||||||
High-Velocity Autonomous Building Canvas
|
|
||||||
</h3>
|
|
||||||
<p style={{ margin: 0, color: 'var(--muted-foreground)', fontSize: 'var(--text-sm)' }}>
|
|
||||||
Rendered on the user-defined <Kbd>--dot-color</Kbd> canvas with composable primitives.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="button primary" onClick={copyPrompt}>
|
|
||||||
<Icon name={copied ? 'check' : 'copy'} size={14} />
|
|
||||||
{copied ? 'Copied Prompt!' : 'Copy Copilot Prompt'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Component Primitives Gallery ── */}
|
<div className="row-xs" style={{ alignItems: 'center' }}>
|
||||||
<div className="grid-2">
|
<Badge variant="outline">Interactive Component</Badge>
|
||||||
<Card>
|
<Badge variant="primary">Composable</Badge>
|
||||||
<CardHeader>
|
|
||||||
<div className="flex-between">
|
|
||||||
<CardTitle>Autonomous Agent Run</CardTitle>
|
|
||||||
<Badge variant="positive">Active</Badge>
|
|
||||||
</div>
|
|
||||||
<CardDescription>
|
|
||||||
Executing token validation and layout synthesis across 4 workers.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="stack-sm">
|
|
||||||
<div className="flex-between" style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)' }}>
|
|
||||||
<span>Token Verification</span>
|
|
||||||
<span style={{ color: 'var(--primary)', fontFamily: 'var(--font-mono)' }}>PASS (0 errors)</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-between" style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)' }}>
|
|
||||||
<span>Color Engine</span>
|
|
||||||
<span style={{ fontFamily: 'var(--font-mono)' }}>CSS Color 4 lab()</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-between" style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)' }}>
|
|
||||||
<span>Theme Switching</span>
|
|
||||||
<span style={{ fontFamily: 'var(--font-mono)' }}>.dark & [data-theme]</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<span style={{ fontSize: 'var(--text-3xs)', color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
|
|
||||||
Press <Kbd>⌘</Kbd> + <Kbd>K</Kbd> for palette
|
|
||||||
</span>
|
|
||||||
<button className="button secondary" style={{ padding: 'var(--space-1) var(--space-3)' }}>
|
|
||||||
View Spec
|
|
||||||
</button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex-between">
|
|
||||||
<CardTitle>Smart Primitives & Status</CardTitle>
|
|
||||||
<Badge variant="secondary">Composable</Badge>
|
|
||||||
</div>
|
|
||||||
<CardDescription>
|
|
||||||
Zero-drift badges, cards, and keyboard indicators ready for prompt generation.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="stack-sm">
|
|
||||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)', marginBottom: 'var(--space-1)' }}>
|
|
||||||
Badge Variants:
|
|
||||||
</div>
|
|
||||||
<div className="row-xs" style={{ flexWrap: 'wrap', gap: 'var(--space-2)' }}>
|
|
||||||
<Badge variant="primary">Primary</Badge>
|
|
||||||
<Badge variant="positive">Positive</Badge>
|
|
||||||
<Badge variant="secondary">Secondary</Badge>
|
|
||||||
<Badge variant="destructive">Destructive</Badge>
|
|
||||||
<Badge variant="outline">Outline</Badge>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)', marginTop: 'var(--space-3)', marginBottom: 'var(--space-1)' }}>
|
|
||||||
Shortcuts & Flags:
|
|
||||||
</div>
|
|
||||||
<div className="row-xs" style={{ gap: 'var(--space-2)' }}>
|
|
||||||
<Kbd>Shift</Kbd> + <Kbd>D</Kbd>
|
|
||||||
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)' }}>toggle theme</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<span style={{ fontSize: 'var(--text-3xs)', color: 'var(--chart-2)', fontFamily: 'var(--font-mono)' }}>
|
|
||||||
✓ Strict token contract compliant
|
|
||||||
</span>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── AI Quick Prompt Recipes ── */}
|
{/* Render Active Real Example Component */}
|
||||||
<div className="code-card">
|
<div className="surface-card" style={{ padding: 'var(--space-6)', borderRadius: 'var(--radius-xl)' }}>
|
||||||
<div className="code-head">
|
{activePattern === 'metrics' && <MetricsDashboard />}
|
||||||
<div className="segmented">
|
|
||||||
{AI_QUICK_RECIPES.map((recipe, idx) => (
|
{activePattern === 'settings' && <SettingsPanel />}
|
||||||
<button
|
|
||||||
key={recipe.title}
|
{activePattern === 'table' && <ServiceTable />}
|
||||||
className={activeRecipe === idx ? 'selected' : ''}
|
|
||||||
onClick={() => setActiveRecipe(idx)}
|
{activePattern === 'deploy' && (
|
||||||
>
|
<div className="stack-md" style={{ maxWidth: 640, margin: '0 auto', textAlign: 'center', padding: 'var(--space-6) 0' }}>
|
||||||
{recipe.title}
|
<Card>
|
||||||
</button>
|
<CardHeader>
|
||||||
))}
|
<CardTitle>Canary Deployment Orchestrator</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
A complete interactive modal flow demonstrating Dialog, Alert, Switch, and Progress components.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div style={{ padding: 'var(--space-6) 0' }}>
|
||||||
|
<DeploymentModal />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<button
|
)}
|
||||||
className="copy-button"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard?.writeText(AI_QUICK_RECIPES[activeRecipe].code)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name={copied ? 'check' : 'copy'} size={13} />
|
|
||||||
{copied ? 'Copied' : 'Copy Recipe'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<pre>
|
|
||||||
<code>{AI_QUICK_RECIPES[activeRecipe].code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+131
-26
@@ -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<string, string> = {
|
export type { LucideIcon, LucideProps }
|
||||||
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',
|
const iconMap: Record<string, LucideIcon> = {
|
||||||
moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z',
|
// Northstar legacy shorthands
|
||||||
copy: 'M8 8h10v12H8zM6 16H4V4h12v2',
|
search: Search,
|
||||||
check: 'm5 12 4 4L19 6',
|
sun: Sun,
|
||||||
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',
|
moon: Moon,
|
||||||
arrow: 'M5 12h14m-6-6 6 6-6 6',
|
copy: Copy,
|
||||||
menu: 'M4 6h16M4 12h16M4 18h16',
|
check: Check,
|
||||||
layers: 'm12 3 9 5-9 5-9-5 9-5Zm-9 9 9 5 9-5M3 16l9 5 9-5',
|
eye: Eye,
|
||||||
sparkle: 'm12 3 2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3Z',
|
arrow: ArrowRight,
|
||||||
terminal: 'm4 17 6-6-6-6m8 14h8',
|
'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<SVGProps<SVGSVGElement>, '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 (
|
return (
|
||||||
<svg
|
<Component
|
||||||
width={size}
|
size={size}
|
||||||
height={size}
|
strokeWidth={strokeWidth}
|
||||||
viewBox="0 0 24 24"
|
className={className}
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.8"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
{...props}
|
||||||
<path d={paths[name] || paths.layers} />
|
/>
|
||||||
</svg>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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<Category>('All')
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [size, setSize] = useState<number>(20)
|
||||||
|
const [copiedName, setCopiedName] = useState<string | null>(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 (
|
||||||
|
<div className="icon-matrix-container">
|
||||||
|
{/* Header controls */}
|
||||||
|
<div className="icon-matrix-header">
|
||||||
|
<div className="icon-search-bar">
|
||||||
|
<Icon name="search" size={16} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search icons (e.g. arrow, check, sparkles)..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="icon-search-input"
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<button className="icon-clear-btn" onClick={() => setSearch('')}>
|
||||||
|
<Icon name="close" size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="icon-size-picker">
|
||||||
|
<span className="mono muted">Size:</span>
|
||||||
|
{[16, 20, 24].map(s => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
className={`icon-size-btn ${size === s ? 'active' : ''}`}
|
||||||
|
onClick={() => setSize(s)}
|
||||||
|
>
|
||||||
|
{s}px
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category tabs */}
|
||||||
|
<div className="icon-category-tabs">
|
||||||
|
{CATEGORIES.map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
className={`icon-cat-btn ${activeCategory === cat ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveCategory(cat)}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
<span className="icon-cat-count">
|
||||||
|
{cat === 'All' ? ICONS.length : ICONS.filter(i => i.category === cat).length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of icons */}
|
||||||
|
<div className="icon-grid">
|
||||||
|
{filteredIcons.map(icon => {
|
||||||
|
const isCopied = copiedName === icon.name
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={icon.name}
|
||||||
|
className={`icon-tile ${isCopied ? 'copied' : ''}`}
|
||||||
|
onClick={() => copyUsage(icon)}
|
||||||
|
title={`Click to copy <${icon.pascalName} />`}
|
||||||
|
>
|
||||||
|
<div className="icon-tile-glyph">
|
||||||
|
{isCopied ? (
|
||||||
|
<Icon name="check" size={size} className="accent" />
|
||||||
|
) : (
|
||||||
|
<icon.component size={size} strokeWidth={1.8} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="icon-tile-name">{icon.name}</span>
|
||||||
|
<span className="icon-tile-action">
|
||||||
|
{isCopied ? 'Copied!' : <Kbd>Copy</Kbd>}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredIcons.length === 0 && (
|
||||||
|
<div className="empty-state">
|
||||||
|
<div className="empty-state-icon">
|
||||||
|
<Icon name="search" size={24} />
|
||||||
|
</div>
|
||||||
|
<h4 style={{ margin: '0 0 var(--space-2)' }}>No icons found</h4>
|
||||||
|
<p className="muted" style={{ margin: 0, fontSize: 'var(--text-xs)' }}>
|
||||||
|
No icon matches "{search}". You can use any of Lucide's 1,400+ icons directly via{' '}
|
||||||
|
<code>lucide-react</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer usage tip */}
|
||||||
|
<div className="icon-matrix-footer">
|
||||||
|
<div className="icon-footer-pill">
|
||||||
|
<Badge variant="outline">lucide-react</Badge>
|
||||||
|
<span>Over 1,400+ SVG icons available. Fully tree-shaken and styled via <code>currentColor</code>.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<HTMLInputElement>(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 (
|
||||||
|
<div className="search-palette-backdrop" onClick={onClose}>
|
||||||
|
<div className="search-palette-modal" onClick={e => e.stopPropagation()}>
|
||||||
|
{/* Search Input Bar */}
|
||||||
|
<div className="search-palette-head">
|
||||||
|
<Icon name="search" size={18} className="search-palette-icon" />
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
className="search-palette-input"
|
||||||
|
placeholder="Type a command, component, or token..."
|
||||||
|
value={query}
|
||||||
|
onChange={e => {
|
||||||
|
setQuery(e.target.value)
|
||||||
|
setSelectedIndex(0)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{query && (
|
||||||
|
<button className="icon-button" onClick={() => setQuery('')} aria-label="Clear search">
|
||||||
|
<Icon name="close" size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<Kbd>ESC</Kbd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results List */}
|
||||||
|
<div className="search-palette-body">
|
||||||
|
{results.length > 0 ? (
|
||||||
|
<div className="search-palette-results">
|
||||||
|
{results.map((item, idx) => {
|
||||||
|
const isSelected = idx === selectedIndex
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={`search-palette-item ${isSelected ? 'selected' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(item)
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setSelectedIndex(idx)}
|
||||||
|
>
|
||||||
|
<div className="search-item-glyph">
|
||||||
|
<Icon name={item.icon} size={16} />
|
||||||
|
</div>
|
||||||
|
<div className="search-item-content">
|
||||||
|
<div className="search-item-title-row">
|
||||||
|
<span className="search-item-title">{item.title}</span>
|
||||||
|
<Badge variant="outline">{item.category}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="search-item-desc">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
{isSelected && (
|
||||||
|
<div className="search-item-action">
|
||||||
|
<Kbd>↵</Kbd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="search-palette-empty">
|
||||||
|
<Icon name="search" size={24} />
|
||||||
|
<p>No results found for “{query}”</p>
|
||||||
|
<span>Try searching for “Buttons”, “Metrics”, “Tokens”, or “Icons”.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer info */}
|
||||||
|
<div className="search-palette-footer">
|
||||||
|
<div className="row-xs" style={{ alignItems: 'center' }}>
|
||||||
|
<Kbd>↑</Kbd>
|
||||||
|
<Kbd>↓</Kbd>
|
||||||
|
<span>to navigate</span>
|
||||||
|
</div>
|
||||||
|
<div className="row-xs" style={{ alignItems: 'center' }}>
|
||||||
|
<Kbd>↵</Kbd>
|
||||||
|
<span>to select</span>
|
||||||
|
</div>
|
||||||
|
<div className="row-xs" style={{ alignItems: 'center' }}>
|
||||||
|
<Kbd>ESC</Kbd>
|
||||||
|
<span>to close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Icon } from './Icon'
|
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[] }
|
export type NavigationGroup = { label: string; items: NavigationItem[] }
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
active: string
|
active: string
|
||||||
groups: NavigationGroup[]
|
groups: NavigationGroup[]
|
||||||
onSelect: (item: string) => void
|
onSelect: (item: NavigationItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ active, groups, onSelect }: SidebarProps) {
|
export function Sidebar({ active, groups, onSelect }: SidebarProps) {
|
||||||
@@ -34,7 +34,7 @@ export function Sidebar({ active, groups, onSelect }: SidebarProps) {
|
|||||||
<button
|
<button
|
||||||
key={item.label}
|
key={item.label}
|
||||||
className={active === item.label ? 'nav-item active' : 'nav-item'}
|
className={active === item.label ? 'nav-item active' : 'nav-item'}
|
||||||
onClick={() => onSelect(item.label)}
|
onClick={() => onSelect(item)}
|
||||||
>
|
>
|
||||||
<span className="nav-glyph" aria-hidden="true">
|
<span className="nav-glyph" aria-hidden="true">
|
||||||
{item.count ? '◈' : '·'}
|
{item.count ? '◈' : '·'}
|
||||||
|
|||||||
+10
-10
@@ -2,8 +2,7 @@ import { useState } from 'react'
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
type TopbarProps = {
|
type TopbarProps = {
|
||||||
query: string
|
onOpenSearch: () => void
|
||||||
onQueryChange: (value: string) => void
|
|
||||||
onMenuToggle: () => void
|
onMenuToggle: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ function useTheme() {
|
|||||||
return { theme, toggle }
|
return { theme, toggle }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Topbar({ query, onQueryChange, onMenuToggle }: TopbarProps) {
|
export function Topbar({ onOpenSearch, onMenuToggle }: TopbarProps) {
|
||||||
const { theme, toggle } = useTheme()
|
const { theme, toggle } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,15 +53,16 @@ export function Topbar({ query, onQueryChange, onMenuToggle }: TopbarProps) {
|
|||||||
<span className="version">v0.8.2</span>
|
<span className="version">v0.8.2</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="top-search">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="top-search"
|
||||||
|
onClick={onOpenSearch}
|
||||||
|
aria-label="Search components, tokens, docs (Press ⌘K)"
|
||||||
|
>
|
||||||
<Icon name="search" size={16} />
|
<Icon name="search" size={16} />
|
||||||
<input
|
<span className="top-search-placeholder">Search components, tokens, docs...</span>
|
||||||
value={query}
|
|
||||||
onChange={e => onQueryChange(e.target.value)}
|
|
||||||
placeholder="Search components, tokens, docs..."
|
|
||||||
/>
|
|
||||||
<kbd>⌘ K</kbd>
|
<kbd>⌘ K</kbd>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<nav className="top-links">
|
<nav className="top-links">
|
||||||
<a href="#docs">Docs</a>
|
<a href="#docs">Docs</a>
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from './ui/Card'
|
||||||
|
import { Badge } from './ui/Badge'
|
||||||
|
import { Kbd } from './ui/Kbd'
|
||||||
|
|
||||||
|
const TYPE_SCALE = [
|
||||||
|
{ token: '--text-hero', rem: 'clamp(2.625rem, 5vw, 4.375rem)', px: '42→70px', role: 'Display / Hero Title', sample: 'Calm, capable design.' },
|
||||||
|
{ token: '--text-2xl', rem: '1.75rem', px: '28px', role: 'Section Headlines', sample: 'Structured for AI Uniformity' },
|
||||||
|
{ token: '--text-xl', rem: '1.5625rem', px: '25px', role: 'Sub-headlines & Major Cards', sample: 'Fast, token-enforced iteration' },
|
||||||
|
{ token: '--text-lg', rem: '1.3125rem', px: '21px', role: 'Card Titles & Dialog Heads', sample: 'Zero hardcoded pixel values' },
|
||||||
|
{ token: '--text-base', rem: '1rem', px: '16px', role: 'Lede Copy & Lead Sentences', sample: 'The baseline font size scaled by --root-font-size.' },
|
||||||
|
{ token: '--text-md', rem: '0.875rem', px: '14px', role: 'Longform Body Text', sample: 'Engineered for dense technical dashboards and documentation.' },
|
||||||
|
{ token: '--text-sm', rem: '0.75rem', px: '12px', role: 'Form Controls & Descriptions', sample: 'Input labels, table rows, and secondary details.' },
|
||||||
|
{ token: '--text-xs', rem: '0.6875rem', px: '11px', role: 'Action Buttons & Nav Items', sample: 'Action buttons, status badges, and sidebar navigation.' },
|
||||||
|
{ token: '--text-2xs', rem: '0.625rem', px: '10px', role: 'Eyebrows & Section Indices', sample: 'METADATA LABELS & CATEGORY EYEBROWS' },
|
||||||
|
{ token: '--text-3xs', rem: '0.5625rem', px: '9px', role: 'Micro Badges & Mono Tags', sample: '01 / APPLICATION PATTERNS' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function TypographySection() {
|
||||||
|
return (
|
||||||
|
<div className="stack-lg">
|
||||||
|
<div className="grid-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex-between">
|
||||||
|
<CardTitle>Sans-Serif Primary Family</CardTitle>
|
||||||
|
<Badge variant="primary">Manrope</Badge>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Modern geometric grotesque optimized for high-density UI and numeral legibility.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div style={{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-lg)', fontWeight: 600 }}>
|
||||||
|
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
<br />
|
||||||
|
abcdefghijklmnopqrstuvwxyz
|
||||||
|
<br />
|
||||||
|
0123456789 & @ # % $ /
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex-between">
|
||||||
|
<CardTitle>Monospace Code Family</CardTitle>
|
||||||
|
<Badge variant="secondary">DM Mono</Badge>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Tabular numerals and proportional spacing for code blocks, tokens, and telemetry.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="mono" style={{ fontSize: 'var(--text-md)', color: 'var(--primary)' }}>
|
||||||
|
const system = 'northstar'
|
||||||
|
<br />
|
||||||
|
latency: 14.2ms | memory: 128MB
|
||||||
|
<br />
|
||||||
|
0123456789 ()=>{}[]
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex-between">
|
||||||
|
<CardTitle>Modular Rem-Based Type Scale</CardTitle>
|
||||||
|
<div className="row-xs">
|
||||||
|
<span className="mono muted" style={{ fontSize: 'var(--text-3xs)' }}>Root:</span>
|
||||||
|
<Kbd>112%</Kbd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Scales uniformly via <code>--root-font-size</code>. Built on a 9px → 70px responsive rhythm.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="stack-md">
|
||||||
|
{TYPE_SCALE.map(item => (
|
||||||
|
<div
|
||||||
|
key={item.token}
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '160px 80px 1fr',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
gap: 'var(--space-4)',
|
||||||
|
padding: 'var(--space-3) 0',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<code style={{ fontSize: 'var(--text-xs)', color: 'var(--primary)', fontFamily: 'var(--font-mono)' }}>
|
||||||
|
{item.token}
|
||||||
|
</code>
|
||||||
|
<div style={{ fontSize: 'var(--text-3xs)', color: 'var(--muted-foreground)' }}>
|
||||||
|
{item.role}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mono muted" style={{ fontSize: 'var(--text-3xs)' }}>
|
||||||
|
{item.px}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: `var(${item.token})`,
|
||||||
|
fontWeight: item.token.includes('hero') || item.token.includes('2xl') ? 800 : 500,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.sample}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogClose,
|
||||||
|
} from '../ui/Dialog'
|
||||||
|
import { Alert, AlertTitle, AlertDescription } from '../ui/Alert'
|
||||||
|
import { Switch } from '../ui/Switch'
|
||||||
|
import { Progress } from '../ui/Progress'
|
||||||
|
import { Badge } from '../ui/Badge'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
|
export function DeploymentModal() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [canaryRouting, setCanaryRouting] = useState(true)
|
||||||
|
const [skipTests, setSkipTests] = useState(false)
|
||||||
|
const [deploying, setDeploying] = useState(false)
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
const [deployed, setDeployed] = useState(false)
|
||||||
|
|
||||||
|
const handleStartDeploy = () => {
|
||||||
|
setDeploying(true)
|
||||||
|
setProgress(15)
|
||||||
|
setTimeout(() => setProgress(45), 500)
|
||||||
|
setTimeout(() => setProgress(85), 1000)
|
||||||
|
setTimeout(() => {
|
||||||
|
setProgress(100)
|
||||||
|
setDeploying(false)
|
||||||
|
setDeployed(true)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setOpen(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
setDeploying(false)
|
||||||
|
setProgress(0)
|
||||||
|
setDeployed(false)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<button className="button primary">
|
||||||
|
<Icon name="sparkles" size={15} /> Promote Release to Canary
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex-between" style={{ marginBottom: 'var(--space-2)' }}>
|
||||||
|
<DialogTitle>Canary Rollout: release-2026.09.12</DialogTitle>
|
||||||
|
<Badge variant="primary">v2.4.0</Badge>
|
||||||
|
</div>
|
||||||
|
<DialogDescription>
|
||||||
|
Targeting production cluster <code style={{ color: 'var(--primary)', fontFamily: 'var(--font-mono)' }}>canary-prod-01</code>.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="stack-md" style={{ margin: 'var(--space-4) 0' }}>
|
||||||
|
{!deployed ? (
|
||||||
|
<>
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertTitle>Canary Traffic Notice</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
This deployment will divert 10% of production ingress traffic to the new build.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="stack-sm" style={{ background: 'var(--secondary)', padding: 'var(--space-4)', borderRadius: 'var(--radius-lg)' }}>
|
||||||
|
<div className="flex-between">
|
||||||
|
<span style={{ fontSize: 'var(--text-xs)', fontWeight: 500 }}>
|
||||||
|
Gradual Traffic Ingress (10% → 50% → 100%)
|
||||||
|
</span>
|
||||||
|
<Switch checked={canaryRouting} onCheckedChange={setCanaryRouting} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-between" style={{ paddingTop: 'var(--space-2)', borderTop: '1px solid var(--border)' }}>
|
||||||
|
<span style={{ fontSize: 'var(--text-xs)', fontWeight: 500 }}>
|
||||||
|
Bypass Pre-Flight Synthetic Probes
|
||||||
|
</span>
|
||||||
|
<Switch checked={skipTests} onCheckedChange={setSkipTests} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deploying && (
|
||||||
|
<div className="stack-xs">
|
||||||
|
<Progress value={progress} label="Applying Canary Weights..." />
|
||||||
|
<span className="mono muted" style={{ fontSize: 'var(--text-3xs)' }}>
|
||||||
|
Streaming build manifests to regional edge nodes...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Alert variant="positive">
|
||||||
|
<AlertTitle>Deployment Succeeded!</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Build is now receiving live canary traffic. Telemetry reports 0 errors and 14ms latency.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{!deployed ? (
|
||||||
|
<>
|
||||||
|
<DialogClose>
|
||||||
|
<button className="button secondary" disabled={deploying}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</DialogClose>
|
||||||
|
<button
|
||||||
|
className="button primary"
|
||||||
|
onClick={handleStartDeploy}
|
||||||
|
disabled={deploying}
|
||||||
|
>
|
||||||
|
<Icon name={deploying ? 'refresh' : 'arrow-right'} size={14} />
|
||||||
|
{deploying ? 'Deploying...' : 'Confirm Rollout'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className="button primary" style={{ width: '100%' }} onClick={handleReset}>
|
||||||
|
<Icon name="check" size={14} /> Done
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { StatCard } from '../ui/StatCard'
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/Card'
|
||||||
|
import { Progress } from '../ui/Progress'
|
||||||
|
import { Badge } from '../ui/Badge'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
|
export type MetricsDashboardProps = {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricsDashboard({
|
||||||
|
title = 'System Overview',
|
||||||
|
description = 'Real-time telemetry and resource saturation across clusters.',
|
||||||
|
className = '',
|
||||||
|
}: MetricsDashboardProps) {
|
||||||
|
const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d'>('24h')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`stack-lg ${className}`}>
|
||||||
|
{/* Dashboard Topbar */}
|
||||||
|
<div className="flex-between" style={{ flexWrap: 'wrap', gap: 'var(--space-3)' }}>
|
||||||
|
<div>
|
||||||
|
<div className="row-xs" style={{ marginBottom: 'var(--space-1)' }}>
|
||||||
|
<Badge variant="positive">Live Telemetry</Badge>
|
||||||
|
<Badge variant="outline">All Regions</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 style={{ margin: 'var(--space-1) 0', fontSize: 'var(--text-lg)', fontWeight: 700 }}>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p style={{ margin: 0, color: 'var(--muted-foreground)', fontSize: 'var(--text-xs)' }}>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row-xs">
|
||||||
|
{(['24h', '7d', '30d'] as const).map(range => (
|
||||||
|
<button
|
||||||
|
key={range}
|
||||||
|
className={`button ${timeRange === range ? 'secondary' : 'ghost'}`}
|
||||||
|
style={{ padding: 'var(--space-1) var(--space-3)', fontSize: 'var(--text-xs)' }}
|
||||||
|
onClick={() => setTimeRange(range)}
|
||||||
|
>
|
||||||
|
{range}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stat Cards Row */}
|
||||||
|
<div className="grid-3">
|
||||||
|
<StatCard
|
||||||
|
title="Throughput"
|
||||||
|
value={timeRange === '24h' ? '14.2k req/s' : timeRange === '7d' ? '12.8k req/s' : '11.5k req/s'}
|
||||||
|
change="+18.4%"
|
||||||
|
trend="up"
|
||||||
|
caption="vs previous period"
|
||||||
|
icon={<Icon name="sparkles" size={16} />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="P99 Latency"
|
||||||
|
value={timeRange === '24h' ? '18ms' : timeRange === '7d' ? '22ms' : '25ms'}
|
||||||
|
change="-4.2ms"
|
||||||
|
trend="up"
|
||||||
|
caption="edge distribution"
|
||||||
|
icon={<Icon name="clock" size={16} />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Error Rate"
|
||||||
|
value="0.008%"
|
||||||
|
change="-0.002%"
|
||||||
|
trend="up"
|
||||||
|
caption="within SLA (<0.05%)"
|
||||||
|
icon={<Icon name="shield" size={16} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cluster Resource Gauges Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex-between">
|
||||||
|
<CardTitle>Resource Utilization</CardTitle>
|
||||||
|
<span className="mono muted" style={{ fontSize: 'var(--text-3xs)' }}>Cluster: canary-prod-01</span>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Compute, memory, and persistent volume headroom.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="stack-md">
|
||||||
|
<div>
|
||||||
|
<div className="flex-between" style={{ marginBottom: 'var(--space-1)' }}>
|
||||||
|
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--foreground)', fontWeight: 500 }}>
|
||||||
|
Worker CPU Saturation
|
||||||
|
</span>
|
||||||
|
<span className="mono" style={{ fontSize: 'var(--text-xs)', color: 'var(--primary)' }}>
|
||||||
|
48%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={48} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex-between" style={{ marginBottom: 'var(--space-1)' }}>
|
||||||
|
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--foreground)', fontWeight: 500 }}>
|
||||||
|
Memory Allocation (16 GB Pool)
|
||||||
|
</span>
|
||||||
|
<span className="mono" style={{ fontSize: 'var(--text-xs)', color: 'var(--primary)' }}>
|
||||||
|
74%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={74} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex-between" style={{ marginBottom: 'var(--space-1)' }}>
|
||||||
|
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--foreground)', fontWeight: 500 }}>
|
||||||
|
Network Ingress Bandwidth
|
||||||
|
</span>
|
||||||
|
<span className="mono" style={{ fontSize: 'var(--text-xs)', color: 'var(--chart-2)' }}>
|
||||||
|
28%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={28} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { useState, useMemo } from 'react'
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../ui/Table'
|
||||||
|
import { Badge } from '../ui/Badge'
|
||||||
|
import { Kbd } from '../ui/Kbd'
|
||||||
|
import { EmptyState, EmptyStateIcon, EmptyStateTitle, EmptyStateDescription } from '../ui/EmptyState'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
|
type Service = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
region: string
|
||||||
|
status: 'healthy' | 'degraded' | 'scaling'
|
||||||
|
latency: string
|
||||||
|
memory: string
|
||||||
|
instances: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_SERVICES: Service[] = [
|
||||||
|
{ id: '1', name: 'api-gateway', region: 'us-east-1', status: 'healthy', latency: '12ms', memory: '128 MB', instances: 8 },
|
||||||
|
{ id: '2', name: 'vector-indexer', region: 'eu-central-1', status: 'healthy', latency: '34ms', memory: '890 MB', instances: 4 },
|
||||||
|
{ id: '3', name: 'model-inference', region: 'us-west-2', status: 'scaling', latency: '78ms', memory: '4.2 GB', instances: 16 },
|
||||||
|
{ id: '4', name: 'session-broker', region: 'us-east-1', status: 'healthy', latency: '8ms', memory: '64 MB', instances: 6 },
|
||||||
|
{ id: '5', name: 'analytics-worker', region: 'ap-northeast-1', status: 'degraded', latency: '142ms', memory: '1.8 GB', instances: 2 },
|
||||||
|
{ id: '6', name: 'audit-logger', region: 'eu-west-1', status: 'healthy', latency: '19ms', memory: '210 MB', instances: 3 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function ServiceTable() {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [filterStatus, setFilterStatus] = useState<string>('all')
|
||||||
|
|
||||||
|
const filteredServices = useMemo(() => {
|
||||||
|
return INITIAL_SERVICES.filter(svc => {
|
||||||
|
const matchesSearch = svc.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
svc.region.toLowerCase().includes(search.toLowerCase())
|
||||||
|
const matchesStatus = filterStatus === 'all' || svc.status === filterStatus
|
||||||
|
return matchesSearch && matchesStatus
|
||||||
|
})
|
||||||
|
}, [search, filterStatus])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="stack-md">
|
||||||
|
{/* Table Toolbar */}
|
||||||
|
<div className="flex-between" style={{ flexWrap: 'wrap', gap: 'var(--space-3)' }}>
|
||||||
|
<div className="icon-search-bar" style={{ maxWidth: 320 }}>
|
||||||
|
<Icon name="search" size={15} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter services or regions..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="icon-search-input"
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<button className="icon-clear-btn" onClick={() => setSearch('')}>
|
||||||
|
<Icon name="close" size={13} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row-xs">
|
||||||
|
{['all', 'healthy', 'scaling', 'degraded'].map(st => (
|
||||||
|
<button
|
||||||
|
key={st}
|
||||||
|
className={`button ${filterStatus === st ? 'secondary' : 'ghost'}`}
|
||||||
|
style={{
|
||||||
|
padding: 'var(--space-1) var(--space-3)',
|
||||||
|
fontSize: 'var(--text-xs)',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
}}
|
||||||
|
onClick={() => setFilterStatus(st)}
|
||||||
|
>
|
||||||
|
{st}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Surface */}
|
||||||
|
{filteredServices.length > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Service Name</TableHead>
|
||||||
|
<TableHead>Region</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>P99 Latency</TableHead>
|
||||||
|
<TableHead>Memory</TableHead>
|
||||||
|
<TableHead>Nodes</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredServices.map(svc => (
|
||||||
|
<TableRow key={svc.id}>
|
||||||
|
<TableCell style={{ fontWeight: 600 }}>
|
||||||
|
<div className="row-xs" style={{ alignItems: 'center' }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: svc.status === 'healthy' ? 'var(--chart-2)' : svc.status === 'scaling' ? 'var(--primary)' : 'var(--destructive)',
|
||||||
|
boxShadow: `0 0 8px ${svc.status === 'healthy' ? 'var(--chart-2)' : svc.status === 'scaling' ? 'var(--primary)' : 'var(--destructive)'}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{svc.name}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Kbd>{svc.region}</Kbd>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={svc.status === 'healthy' ? 'positive' : svc.status === 'scaling' ? 'primary' : 'destructive'}
|
||||||
|
>
|
||||||
|
{svc.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="mono" style={{ color: svc.status === 'degraded' ? 'var(--destructive)' : 'var(--primary)' }}>
|
||||||
|
{svc.latency}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="mono" style={{ color: 'var(--muted-foreground)' }}>
|
||||||
|
{svc.memory}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="mono" style={{ color: 'var(--foreground)' }}>
|
||||||
|
{svc.instances}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<EmptyState>
|
||||||
|
<EmptyStateIcon>
|
||||||
|
<Icon name="search" size={24} />
|
||||||
|
</EmptyStateIcon>
|
||||||
|
<EmptyStateTitle>No matching services</EmptyStateTitle>
|
||||||
|
<EmptyStateDescription>
|
||||||
|
No active nodes or services match the current filter query "{search}".
|
||||||
|
</EmptyStateDescription>
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-between muted" style={{ fontSize: 'var(--text-3xs)', padding: '0 var(--space-2)' }}>
|
||||||
|
<span>Showing {filteredServices.length} of {INITIAL_SERVICES.length} deployed services</span>
|
||||||
|
<span className="mono">Synced live via cluster coordinator</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../ui/Card'
|
||||||
|
import { Switch } from '../ui/Switch'
|
||||||
|
import { Badge } from '../ui/Badge'
|
||||||
|
import { Alert, AlertTitle, AlertDescription } from '../ui/Alert'
|
||||||
|
import { Tooltip } from '../ui/Tooltip'
|
||||||
|
import { Kbd } from '../ui/Kbd'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
|
export function SettingsPanel() {
|
||||||
|
const [apiKeyVisible, setApiKeyVisible] = useState(false)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [saved, setSaved] = useState(false)
|
||||||
|
|
||||||
|
// Toggles
|
||||||
|
const [canaryEnabled, setCanaryEnabled] = useState(true)
|
||||||
|
const [telemetryEnabled, setTelemetryEnabled] = useState(true)
|
||||||
|
const [autoRollback, setAutoRollback] = useState(false)
|
||||||
|
|
||||||
|
const apiKey = 'nsk_live_98a72ef01bc34d89a2'
|
||||||
|
|
||||||
|
const copyKey = () => {
|
||||||
|
navigator.clipboard?.writeText(apiKey)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setSaved(true)
|
||||||
|
setTimeout(() => setSaved(false), 2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="stack-lg">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex-between">
|
||||||
|
<CardTitle>Environment & Access Keys</CardTitle>
|
||||||
|
<Badge variant="primary">Production</Badge>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Manage authentication credentials and runtime ingress access.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="stack-md">
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', fontSize: 'var(--text-xs)', fontWeight: 600, marginBottom: 'var(--space-2)' }}>
|
||||||
|
Production Secret API Key
|
||||||
|
</label>
|
||||||
|
<div className="row-xs" style={{ alignItems: 'center' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: 'var(--secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
padding: 'var(--space-2) var(--space-3)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 'var(--text-xs)',
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{apiKeyVisible ? apiKey : '••••••••••••••••••••••••••••••••'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="icon-button"
|
||||||
|
style={{ padding: 4 }}
|
||||||
|
onClick={() => setApiKeyVisible(!apiKeyVisible)}
|
||||||
|
aria-label={apiKeyVisible ? 'Hide API key' : 'Show API key'}
|
||||||
|
>
|
||||||
|
<Icon name={apiKeyVisible ? 'eye' : 'eye'} size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Tooltip content="Copy key to clipboard">
|
||||||
|
<button className="button secondary" onClick={copyKey}>
|
||||||
|
<Icon name={copied ? 'check' : 'copy'} size={14} />
|
||||||
|
{copied ? 'Copied' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert variant="info">
|
||||||
|
<AlertTitle>Zero-Trust Rotation Enabled</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Keys are automatically renewed every 90 days. Next scheduled rotation in 14 days.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Deployment & Runtime Safeguards</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Fine-tune canary threshold tolerances and telemetry logging.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="stack-md">
|
||||||
|
<div className="flex-between">
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, color: 'var(--foreground)' }}>
|
||||||
|
Automated Canary Progression
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)', marginTop: 2 }}>
|
||||||
|
Gradually increase traffic from 5% to 100% based on error budget.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch checked={canaryEnabled} onCheckedChange={setCanaryEnabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-between" style={{ paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border)' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, color: 'var(--foreground)' }}>
|
||||||
|
Detailed OpenTelemetry Tracing
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)', marginTop: 2 }}>
|
||||||
|
Emit span metrics for all inbound HTTP and gRPC remote procedure calls.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch checked={telemetryEnabled} onCheckedChange={setTelemetryEnabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-between" style={{ paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border)' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, color: 'var(--foreground)' }}>
|
||||||
|
Immediate Circuit-Breaker Rollback
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)', marginTop: 2 }}>
|
||||||
|
Instantly revert to prior green build if P99 latency breaches 100ms.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch checked={autoRollback} onCheckedChange={setAutoRollback} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<span style={{ fontSize: 'var(--text-3xs)', color: 'var(--muted-foreground)' }}>
|
||||||
|
Shortcut: <Kbd>Ctrl</Kbd> + <Kbd>S</Kbd> to persist
|
||||||
|
</span>
|
||||||
|
<div className="row-xs" style={{ marginLeft: 'auto' }}>
|
||||||
|
<button className="button ghost">Discard</button>
|
||||||
|
<button className="button primary" onClick={handleSave}>
|
||||||
|
<Icon name={saved ? 'check' : 'sparkles'} size={14} />
|
||||||
|
{saved ? 'Saved Successfully' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './MetricsDashboard'
|
||||||
|
export * from './SettingsPanel'
|
||||||
|
export * from './ServiceTable'
|
||||||
|
export * from './DeploymentModal'
|
||||||
@@ -8,7 +8,7 @@ type DialogContextType = {
|
|||||||
|
|
||||||
const DialogContext = createContext<DialogContextType | null>(null)
|
const DialogContext = createContext<DialogContextType | null>(null)
|
||||||
|
|
||||||
export function useDialog() {
|
function useDialog() {
|
||||||
const ctx = useContext(DialogContext)
|
const ctx = useContext(DialogContext)
|
||||||
if (!ctx) throw new Error('Dialog components must be used within a <Dialog>')
|
if (!ctx) throw new Error('Dialog components must be used within a <Dialog>')
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type SheetContextType = {
|
|||||||
|
|
||||||
const SheetContext = createContext<SheetContextType | null>(null)
|
const SheetContext = createContext<SheetContextType | null>(null)
|
||||||
|
|
||||||
export function useSheet() {
|
function useSheet() {
|
||||||
const ctx = useContext(SheetContext)
|
const ctx = useContext(SheetContext)
|
||||||
if (!ctx) throw new Error('Sheet components must be used within <Sheet>')
|
if (!ctx) throw new Error('Sheet components must be used within <Sheet>')
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type TabsContextType = {
|
|||||||
|
|
||||||
const TabsContext = createContext<TabsContextType | null>(null)
|
const TabsContext = createContext<TabsContextType | null>(null)
|
||||||
|
|
||||||
export function useTabs() {
|
function useTabs() {
|
||||||
const ctx = useContext(TabsContext)
|
const ctx = useContext(TabsContext)
|
||||||
if (!ctx) throw new Error('Tabs components must be used within <Tabs>')
|
if (!ctx) throw new Error('Tabs components must be used within <Tabs>')
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export * from './Skeleton'
|
|||||||
export * from './Table'
|
export * from './Table'
|
||||||
export * from './Sheet'
|
export * from './Sheet'
|
||||||
export * from './Tooltip'
|
export * from './Tooltip'
|
||||||
|
export * from '../Icon'
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
export const AI_SYSTEM_PROMPT = `You are an expert frontend engineer building with Northstar Design System.
|
|
||||||
Key Architectural Principles:
|
|
||||||
1. Single source of truth: All colors, typography, spacing, radii, and shadows derive from src/tokens.css.
|
|
||||||
2. Zero hardcoded colors: NEVER use raw hex (#fff), rgb/rgba, or px font sizes in components or stylesheets.
|
|
||||||
3. Standard design tokens available:
|
|
||||||
- Surfaces: var(--background), var(--card), var(--popover), var(--secondary), var(--sidebar)
|
|
||||||
- Foreground: var(--foreground), var(--card-foreground), var(--muted-foreground)
|
|
||||||
- Brand & Actions: var(--primary), var(--primary-foreground), var(--ring)
|
|
||||||
- Status & Charts: var(--destructive), var(--chart-1) through var(--chart-5)
|
|
||||||
- Spacing: var(--space-1) (4px) to var(--space-11) (80px)
|
|
||||||
- Typography: var(--text-3xs) to var(--text-2xl), var(--font-sans), var(--font-mono)
|
|
||||||
4. Use the cn() utility from '@/utils/cn' for conditional class names.
|
|
||||||
5. Composable UI primitives live in '@/components/ui/' (Badge, Card, StatCard, Kbd).
|
|
||||||
6. Dot canvas pattern: use .dot-canvas or radial-gradient with var(--dot-color).`
|
|
||||||
|
|
||||||
export const AI_QUICK_RECIPES = [
|
|
||||||
{
|
|
||||||
title: 'Composing a Metric Card',
|
|
||||||
code: `<StatCard
|
|
||||||
title="Active Agents"
|
|
||||||
value="1,429"
|
|
||||||
change="+18.4%"
|
|
||||||
trend="up"
|
|
||||||
caption="vs last 7 days"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Using Conditional Classes with cn()',
|
|
||||||
code: `<div className={cn(
|
|
||||||
'surface-card hover-lift',
|
|
||||||
isActive && 'glow-primary',
|
|
||||||
className
|
|
||||||
)}>
|
|
||||||
<Badge variant="primary">Production</Badge>
|
|
||||||
</div>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Applying the Dot Grid Canvas',
|
|
||||||
code: `<div className="dot-canvas p-6 rounded-lg border border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Autonomous Run</CardTitle>
|
|
||||||
<CardDescription>Pipeline executing across 8 nodes</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</div>`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
+35
-5
@@ -3,18 +3,48 @@ import type { Token } from '../components/TokenGrid'
|
|||||||
import type { NavigationGroup } from '../components/Sidebar'
|
import type { NavigationGroup } from '../components/Sidebar'
|
||||||
|
|
||||||
export const navigationGroups: NavigationGroup[] = [
|
export const navigationGroups: NavigationGroup[] = [
|
||||||
{ label: 'Start here', items: [{ label: 'Overview' }, { label: 'Principles' }, { label: 'Quickstart' }] },
|
{
|
||||||
{ label: 'Foundations', items: [{ label: 'Color & theme' }, { label: 'Typography' }, { label: 'Spacing' }, { label: 'Elevation' }] },
|
label: 'Application Patterns',
|
||||||
{ label: 'Components', items: [{ label: 'Buttons', count: 12 }, { label: 'Form controls' }, { label: 'Badges' }, { label: 'Cards' }, { label: 'Data table' }] },
|
items: [
|
||||||
{ label: 'AI Workflow', items: [{ label: 'Smart Utils' }, { label: 'Prompt Recipes' }, { label: 'Copilot Rules' }] },
|
{ label: 'Metrics Dashboard', id: 'application-patterns' },
|
||||||
|
{ label: 'Settings Panel', id: 'application-patterns' },
|
||||||
|
{ label: 'Service Table', id: 'application-patterns' },
|
||||||
|
{ label: 'Deployment Flow', id: 'application-patterns' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Components',
|
||||||
|
items: [
|
||||||
|
{ label: 'Buttons', count: 12, id: 'buttons' },
|
||||||
|
{ label: 'Icons', count: 42, id: 'icons' },
|
||||||
|
{ label: 'Form controls', id: 'form-controls' },
|
||||||
|
{ label: 'Composable code', id: 'composable-by-default' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Foundations',
|
||||||
|
items: [
|
||||||
|
{ label: 'Color tokens', id: 'color-tokens' },
|
||||||
|
{ label: 'Typography', id: 'typography' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Documentation',
|
||||||
|
items: [
|
||||||
|
{ label: 'Markdown rendering', id: 'markdown-rendering' },
|
||||||
|
{ label: 'CodeMirror theme', id: 'codemirror-theme' },
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const buttonTabs = ['Preview', 'API reference', 'Usage']
|
export const buttonTabs = ['Preview', 'API reference', 'Usage']
|
||||||
|
|
||||||
export const pageSections = [
|
export const pageSections = [
|
||||||
'AI Workflow & Smart Utils',
|
'Application Patterns',
|
||||||
'Buttons',
|
'Buttons',
|
||||||
|
'Icons',
|
||||||
'Color tokens',
|
'Color tokens',
|
||||||
|
'Typography',
|
||||||
'Form controls',
|
'Form controls',
|
||||||
'Composable by default',
|
'Composable by default',
|
||||||
'Markdown rendering',
|
'Markdown rendering',
|
||||||
|
|||||||
+2
-2
@@ -77,7 +77,7 @@
|
|||||||
--accent: lab(94.2% 0 0);
|
--accent: lab(94.2% 0 0);
|
||||||
--accent-foreground: lab(7.78201% -.0000149012 0);
|
--accent-foreground: lab(7.78201% -.0000149012 0);
|
||||||
--destructive: lab(48.4493% 77.4328 61.5452);
|
--destructive: lab(48.4493% 77.4328 61.5452);
|
||||||
--destructive-foreground: lab(48.4493% 77.4328 61.5452);
|
--destructive-foreground: lab(98.26% 0 0);
|
||||||
--border: lab(86.08% -.0000298023 0);
|
--border: lab(86.08% -.0000298023 0);
|
||||||
--input: lab(86.08% -.0000298023 0);
|
--input: lab(86.08% -.0000298023 0);
|
||||||
--ring: lab(42% 0 0);
|
--ring: lab(42% 0 0);
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
--accent: lab(1.98454% 0 0);
|
--accent: lab(1.98454% 0 0);
|
||||||
--accent-foreground: lab(89.9321% -47.9163 -15.7303);
|
--accent-foreground: lab(89.9321% -47.9163 -15.7303);
|
||||||
--destructive: lab(54.5811% 71.684 50.9279);
|
--destructive: lab(54.5811% 71.684 50.9279);
|
||||||
--destructive-foreground: lab(54.5811% 71.684 50.9279);
|
--destructive-foreground: lab(100% 0 0);
|
||||||
--border: lab(4.8411% 0 0);
|
--border: lab(4.8411% 0 0);
|
||||||
--input: lab(1.98454% 0 0);
|
--input: lab(1.98454% 0 0);
|
||||||
--ring: lab(89.9321% -47.9163 -15.7303);
|
--ring: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
|||||||
+17
-5
@@ -285,7 +285,8 @@
|
|||||||
/* ── Composable Tabs ────────────────────────────────────────── */
|
/* ── Composable Tabs ────────────────────────────────────────── */
|
||||||
.ui-tabs-list {
|
.ui-tabs-list {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: var(--space-1);
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background: var(--secondary);
|
background: var(--secondary);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -293,16 +294,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ui-tabs-trigger {
|
.ui-tabs-trigger {
|
||||||
border: 0;
|
border: 1px solid transparent;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-sm);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
padding: var(--space-2) var(--space-4);
|
padding: var(--space-2) var(--space-4);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.2s;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
transition: all 0.15s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +318,14 @@
|
|||||||
.ui-tabs-trigger.active {
|
.ui-tabs-trigger.active {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
|
border-color: color-mix(in srgb, var(--border) 60%, transparent);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs-trigger svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-tabs-content {
|
.ui-tabs-content {
|
||||||
|
|||||||
Reference in new issue
Block a user