Template
non ai theme
This commit is contained in:
1 parent
9d444cc0ec
commit
61df5e3dcc
28 files changed
+3347
-448
No files matched your search
@@ -17,26 +17,42 @@ React 19 + Vite 8 + TypeScript design-system showcase. Dark-first, token-driven
|
|||||||
|
|
||||||
## Design tokens — the contract
|
## Design tokens — the contract
|
||||||
|
|
||||||
1. **Single source of truth: `src/tokens.css`.** Every font size, color, spacing, radius, and shadow color in `src/App.css` comes from a token. The guard `scripts/check-tokens.mjs` (in `npm run check`) bans hardcoded hex colors, px font sizes, and raw `rgba()` in application CSS — new colors go in as tokens, accent washes use `color-mix(in srgb, var(--accent) N%, transparent)`.
|
1. **Single source of truth: `src/tokens.css`.** Every font size, color, spacing, radius, and shadow color comes from a token. The palette is defined in modern CSS Color 4 `lab(...)` color space:
|
||||||
2. Typography scale (`--text-3xs` … `--text-2xl`, `--text-hero`) is rem-based on a 9→28px grid. Longform body `--text-md`, buttons/nav `--text-xs`, meta/eyebrows `--text-2xs`/`--text-3xs`.
|
- Dark theme (`.dark`, `[data-theme='dark']`): Pitch black base (`lab(0% 0 0)`), deep cards (`lab(.903296% 0 0)`), radiant electric mint/cyan primary (`lab(89.9321% -47.9163 -15.7303)`), vivid destructive (`lab(54.5811% 71.684 50.9279)`), chart colors 1–5, and subtle dot grid (`--dot-color`).
|
||||||
3. **Global density knob: `--root-font-size`** (default `112%` readability preset, baseline 16px = `100%`). One line scales every token-driven size app-wide; spacing tokens follow.
|
- Light theme (`:root`, `[data-theme='light']`): Pure white daylight base (`lab(100% 0 0)`), crisp charcoal primary (`lab(7.78201% -.0000149012 0)`), and high-contrast surfaces.
|
||||||
4. **Theming:** components read only `var(--*)` and never know the theme. `:root[data-theme='light']` in tokens.css holds the daylight palette. Toggle = `document.documentElement.dataset.theme` (Topbar's sun/moon button, persisted in `localStorage['ns-theme']`). Adding a third theme = one more token block.
|
2. The guard `scripts/check-tokens.mjs` (in `npm run check`) bans hardcoded hex colors, px font sizes, and raw `rgba()` in application CSS — new colors go in as tokens, accent washes use `color-mix(in srgb, var(--primary) N%, transparent)`.
|
||||||
5. "Make the small text a bit larger" = bump `--text-xs`/`--text-2xs`. "Soften a surface" = its token. No grep, no per-component edits.
|
3. Typography scale (`--text-3xs` … `--text-2xl`, `--text-hero`) is rem-based on a 9→28px grid. Longform body `--text-md`, buttons/nav `--text-xs`, meta/eyebrows `--text-2xs`/`--text-3xs`.
|
||||||
|
4. **Global density knob: `--root-font-size`** (default `112%` readability preset, baseline 16px = `100%`). One line scales every token-driven size app-wide; spacing tokens follow.
|
||||||
|
5. **Theming:** components read only `var(--*)` and never know the theme. Toggle synchronizes both `document.documentElement.dataset.theme` and `.dark`/`.light` classes.
|
||||||
|
|
||||||
|
## Smart Utils & AI Workflow
|
||||||
|
|
||||||
|
- **Class helper**: `src/utils/cn.ts` — zero-dependency classnames merging utility for conditional component styles.
|
||||||
|
- **Smart CSS**: `src/utils.css` — ready-to-use helpers (`.dot-canvas`, `.surface-card`, `.surface-sidebar`, `.badge-*`, `.hover-lift`, `.kbd-tag`, `.skeleton-pulse`).
|
||||||
|
- **Composable Primitives**: `src/components/ui/` (`Badge.tsx`, `Card.tsx`, `StatCard.tsx`, `Kbd.tsx`).
|
||||||
|
- **AI Agent System Prompt & Recipes**: `src/data/ai-rules.ts`.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
tokens.css ← design tokens + light theme (edit this first)
|
tokens.css ← lab() design tokens + dark & light themes (edit this first)
|
||||||
index.css ← fonts, tokens import, base resets
|
utils.css ← token-compliant smart CSS utility classes
|
||||||
App.css ← all component styles (token-driven, plain CSS)
|
index.css ← fonts, tokens & utils import, base resets
|
||||||
App.tsx ← page composition only
|
App.css ← application component styles (token-driven, plain CSS)
|
||||||
components/ ← one component per file, readable multi-line JSX
|
App.tsx ← page composition & showcase layout
|
||||||
Topbar.tsx ← owns the theme toggle (useTheme inside)
|
utils/
|
||||||
|
cn.ts ← conditional classnames utility
|
||||||
|
components/
|
||||||
|
ui/ ← composable UI primitives (Badge, Card, StatCard, Kbd)
|
||||||
|
AIWorkflowSection.tsx ← AI workflow and smart utils showcase
|
||||||
|
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/library.ts ← demo content for the showcase
|
data/
|
||||||
|
library.ts ← demo content and color token data for the showcase
|
||||||
|
ai-rules.ts ← AI copilot prompt and quick implementation recipes
|
||||||
scripts/check-tokens.mjs ← token contract guard (part of npm run check)
|
scripts/check-tokens.mjs ← token contract guard (part of npm run check)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1442
-313
File diff suppressed because it is too large.
Load diff
+24
-8
@@ -9,6 +9,7 @@ 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 { Section } from './components/Section'
|
import { Section } from './components/Section'
|
||||||
import { RightRail } from './components/RightRail'
|
import { RightRail } from './components/RightRail'
|
||||||
import { Icon } from './components/Icon'
|
import { Icon } from './components/Icon'
|
||||||
@@ -49,8 +50,18 @@ export default function App() {
|
|||||||
<div className="content-grid">
|
<div className="content-grid">
|
||||||
<div className="content-main">
|
<div className="content-main">
|
||||||
<Section
|
<Section
|
||||||
id="button-matrix"
|
id="ai-workflow-smart-utils"
|
||||||
index="01 / COMPONENTS"
|
index="01 / AI WORKFLOW & SMART UTILS"
|
||||||
|
title="Smart Utils & AI Iteration"
|
||||||
|
description="Engineered for AI copilot uniformity, zero visual drift, and instant composable prototyping."
|
||||||
|
meta={<span className="api-pill">lab() colors · cn() · primitives</span>}
|
||||||
|
>
|
||||||
|
<AIWorkflowSection />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section
|
||||||
|
id="buttons"
|
||||||
|
index="02 / COMPONENTS"
|
||||||
title="Buttons"
|
title="Buttons"
|
||||||
description="Actions with a clear hierarchy and dependable feedback."
|
description="Actions with a clear hierarchy and dependable feedback."
|
||||||
meta={<span className="api-pill">Button · 12 variants</span>}
|
meta={<span className="api-pill">Button · 12 variants</span>}
|
||||||
@@ -65,9 +76,10 @@ export default function App() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
index="02 / FOUNDATIONS"
|
id="color-tokens"
|
||||||
|
index="03 / FOUNDATIONS"
|
||||||
title="Color tokens"
|
title="Color tokens"
|
||||||
description="Functional color roles keep every surface intentional."
|
description="Functional color roles in CSS Color 4 lab() keep every surface intentional."
|
||||||
meta={
|
meta={
|
||||||
<button className="text-button">
|
<button className="text-button">
|
||||||
View all tokens <Icon name="arrow" size={15} />
|
View all tokens <Icon name="arrow" size={15} />
|
||||||
@@ -78,7 +90,8 @@ export default function App() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
index="03 / PRIMITIVES"
|
id="form-controls"
|
||||||
|
index="04 / 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."
|
||||||
>
|
>
|
||||||
@@ -107,7 +120,8 @@ export default function App() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
index="04 / CODE"
|
id="composable-by-default"
|
||||||
|
index="05 / CODE"
|
||||||
title="Composable by default"
|
title="Composable by default"
|
||||||
description="Small APIs. Strong defaults. No surprises."
|
description="Small APIs. Strong defaults. No surprises."
|
||||||
>
|
>
|
||||||
@@ -115,7 +129,8 @@ export default function App() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
index="05 / CONTENT"
|
id="markdown-rendering"
|
||||||
|
index="06 / 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>}
|
||||||
@@ -124,7 +139,8 @@ export default function App() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
index="06 / DEVELOPER TOOLING"
|
id="codemirror-theme"
|
||||||
|
index="07 / 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>}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { StatCard } from './ui/StatCard'
|
||||||
|
import { Badge } from './ui/Badge'
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/Card'
|
||||||
|
import { Kbd } from './ui/Kbd'
|
||||||
|
import { Icon } from './Icon'
|
||||||
|
import { AI_SYSTEM_PROMPT, AI_QUICK_RECIPES } from '../data/ai-rules'
|
||||||
|
|
||||||
|
export function AIWorkflowSection() {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [activeRecipe, setActiveRecipe] = useState(0)
|
||||||
|
|
||||||
|
const copyPrompt = () => {
|
||||||
|
navigator.clipboard?.writeText(AI_SYSTEM_PROMPT)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="stack-lg">
|
||||||
|
{/* ── Metric Highlights using StatCard Primitives ── */}
|
||||||
|
<div className="grid-3">
|
||||||
|
<StatCard
|
||||||
|
title="Iteration Velocity"
|
||||||
|
value="4.8x"
|
||||||
|
change="+42%"
|
||||||
|
trend="up"
|
||||||
|
caption="via zero-drift tokens"
|
||||||
|
icon={<Icon name="sparkle" size={16} />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Token Uniformity"
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* ── Component Primitives Gallery ── */}
|
||||||
|
<div className="grid-2">
|
||||||
|
<Card>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* ── AI Quick Prompt Recipes ── */}
|
||||||
|
<div className="code-card">
|
||||||
|
<div className="code-head">
|
||||||
|
<div className="segmented">
|
||||||
|
{AI_QUICK_RECIPES.map((recipe, idx) => (
|
||||||
|
<button
|
||||||
|
key={recipe.title}
|
||||||
|
className={activeRecipe === idx ? 'selected' : ''}
|
||||||
|
onClick={() => setActiveRecipe(idx)}
|
||||||
|
>
|
||||||
|
{recipe.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ const paths: Record<string, string> = {
|
|||||||
arrow: 'M5 12h14m-6-6 6 6-6 6',
|
arrow: 'M5 12h14m-6-6 6 6-6 6',
|
||||||
menu: 'M4 6h16M4 12h16M4 18h16',
|
menu: 'M4 6h16M4 12h16M4 18h16',
|
||||||
layers: 'm12 3 9 5-9 5-9-5 9-5Zm-9 9 9 5 9-5M3 16l9 5 9-5',
|
layers: 'm12 3 9 5-9 5-9-5 9-5Zm-9 9 9 5 9-5M3 16l9 5 9-5',
|
||||||
|
sparkle: 'm12 3 2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3Z',
|
||||||
|
terminal: 'm4 17 6-6-6-6m8 14h8',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Icon({ name, size = 18 }: IconProps) {
|
export function Icon({ name, size = 18 }: IconProps) {
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ export function RightRail({ sections }: RightRailProps) {
|
|||||||
<aside className="right-rail">
|
<aside className="right-rail">
|
||||||
<div className="rail-card">
|
<div className="rail-card">
|
||||||
<span className="eyebrow">On this page</span>
|
<span className="eyebrow">On this page</span>
|
||||||
{sections.map((label, i) => (
|
{sections.map((label, i) => {
|
||||||
<a className={i === 0 ? 'rail-link active' : 'rail-link'} href={i === 0 ? '#button-matrix' : '#'} key={label}>
|
const id = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
||||||
<span>0{i + 1}</span>
|
return (
|
||||||
{label}
|
<a className={i === 0 ? 'rail-link active' : 'rail-link'} href={`#${id}`} key={label}>
|
||||||
</a>
|
<span>0{i + 1}</span>
|
||||||
))}
|
{label}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rail-card release">
|
<div className="rail-card release">
|
||||||
|
|||||||
@@ -9,13 +9,33 @@ type TopbarProps = {
|
|||||||
|
|
||||||
/** App theme is a data attribute on <html>; tokens.css defines both palettes. */
|
/** App theme is a data attribute on <html>; tokens.css defines both palettes. */
|
||||||
function useTheme() {
|
function useTheme() {
|
||||||
const [theme, setTheme] = useState<'dark' | 'light'>(() =>
|
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
|
||||||
localStorage.getItem('ns-theme') === 'light' ? 'light' : 'dark',
|
const saved = typeof window !== 'undefined' ? localStorage.getItem('ns-theme') : null
|
||||||
)
|
const initial = saved === 'light' ? 'light' : 'dark'
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.dataset.theme = initial
|
||||||
|
if (initial === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
document.documentElement.classList.remove('light')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('light')
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return initial
|
||||||
|
})
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
const next = theme === 'dark' ? 'light' : 'dark'
|
const next = theme === 'dark' ? 'light' : 'dark'
|
||||||
setTheme(next)
|
setTheme(next)
|
||||||
document.documentElement.dataset.theme = next
|
document.documentElement.dataset.theme = next
|
||||||
|
if (next === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
document.documentElement.classList.remove('light')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('light')
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
localStorage.setItem('ns-theme', next)
|
localStorage.setItem('ns-theme', next)
|
||||||
}
|
}
|
||||||
return { theme, toggle }
|
return { theme, toggle }
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { type HTMLAttributes, type ReactNode } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
export type AlertVariant = 'info' | 'positive' | 'warning' | 'destructive'
|
||||||
|
|
||||||
|
type AlertProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
variant?: AlertVariant
|
||||||
|
icon?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Alert({ variant = 'info', icon, className, children, ...props }: AlertProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('alert', `alert-${variant}`, className)} role="alert" {...props}>
|
||||||
|
{icon && <div className="alert-icon" style={{ display: 'flex', alignItems: 'center' }}>{icon}</div>}
|
||||||
|
<div className="stack-xs" style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return (
|
||||||
|
<h5
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-sm)',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h5>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-xs)',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
export type BadgeVariant = 'primary' | 'secondary' | 'destructive' | 'positive' | 'outline'
|
||||||
|
|
||||||
|
type BadgeProps = {
|
||||||
|
children: ReactNode
|
||||||
|
variant?: BadgeVariant
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge({ children, variant = 'primary', className }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<span className={cn('badge', `badge-${variant}`, className)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('surface-card', className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('stack-xs', className)} style={{ marginBottom: 'var(--space-4)' }} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-lg)',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-sm)',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn(className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex-between', className)}
|
||||||
|
style={{
|
||||||
|
marginTop: 'var(--space-5)',
|
||||||
|
paddingTop: 'var(--space-4)',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect, type ReactNode, type HTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
type DialogContextType = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogContext = createContext<DialogContextType | null>(null)
|
||||||
|
|
||||||
|
export function useDialog() {
|
||||||
|
const ctx = useContext(DialogContext)
|
||||||
|
if (!ctx) throw new Error('Dialog components must be used within a <Dialog>')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DialogProps = {
|
||||||
|
children: ReactNode
|
||||||
|
open?: boolean
|
||||||
|
defaultOpen?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dialog({ children, open: controlledOpen, defaultOpen = false, onOpenChange }: DialogProps) {
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)
|
||||||
|
const isControlled = controlledOpen !== undefined
|
||||||
|
const open = isControlled ? controlledOpen : uncontrolledOpen
|
||||||
|
|
||||||
|
const setOpen = (next: boolean) => {
|
||||||
|
if (!isControlled) setUncontrolledOpen(next)
|
||||||
|
onOpenChange?.(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContext.Provider value={{ open, setOpen }}>
|
||||||
|
{children}
|
||||||
|
</DialogContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogTrigger({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
const { setOpen } = useDialog()
|
||||||
|
return (
|
||||||
|
<span className={className} onClick={() => setOpen(true)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogContent({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
const { open, setOpen } = useDialog()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') setOpen(false)
|
||||||
|
}
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}, [open, setOpen])
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dialog-backdrop" onClick={() => setOpen(false)}>
|
||||||
|
<div className={cn('dialog-panel', className)} onClick={e => e.stopPropagation()}>
|
||||||
|
<button
|
||||||
|
className="dialog-close-btn"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
aria-label="Close dialog"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('stack-xs', className)} style={{ marginBottom: 'var(--space-4)' }} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-xl)',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
letterSpacing: '-0.03em',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-sm)',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex-between', className)}
|
||||||
|
style={{
|
||||||
|
marginTop: 'var(--space-6)',
|
||||||
|
paddingTop: 'var(--space-4)',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
gap: 'var(--space-3)',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogClose({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
const { setOpen } = useDialog()
|
||||||
|
return (
|
||||||
|
<span className={className} onClick={() => setOpen(false)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { HTMLAttributes, ReactNode } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
export function EmptyState({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('empty-state', className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyStateIcon({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
return <div className={cn('empty-state-icon', className)}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyStateTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: '0 0 var(--space-2)',
|
||||||
|
fontSize: 'var(--text-lg)',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyStateDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: '0 0 var(--space-5)',
|
||||||
|
fontSize: 'var(--text-sm)',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
maxWidth: '420px',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyStateActions({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('row-sm', className)} {...props} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
export function Kbd({ className, ...props }: HTMLAttributes<HTMLElement>) {
|
||||||
|
return <kbd className={cn('kbd-tag', className)} {...props} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
type ProgressProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
value?: number
|
||||||
|
max?: number
|
||||||
|
indeterminate?: boolean
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Progress({
|
||||||
|
value = 0,
|
||||||
|
max = 100,
|
||||||
|
indeterminate = false,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ProgressProps) {
|
||||||
|
const percentage = Math.min(Math.max(Math.round((value / max) * 100), 0), 100)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('stack-xs', className)} {...props}>
|
||||||
|
{(label || !indeterminate) && (
|
||||||
|
<div className="flex-between" style={{ fontSize: 'var(--text-xs)', color: 'var(--muted-foreground)' }}>
|
||||||
|
{label && <span>{label}</span>}
|
||||||
|
{!indeterminate && <span style={{ fontFamily: 'var(--font-mono)' }}>{percentage}%</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="progress-container">
|
||||||
|
<div
|
||||||
|
className={cn('progress-fill', indeterminate && 'progress-indeterminate')}
|
||||||
|
style={!indeterminate ? { width: `${percentage}%` } : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect, type ReactNode, type HTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
type SheetContextType = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SheetContext = createContext<SheetContextType | null>(null)
|
||||||
|
|
||||||
|
export function useSheet() {
|
||||||
|
const ctx = useContext(SheetContext)
|
||||||
|
if (!ctx) throw new Error('Sheet components must be used within <Sheet>')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
type SheetProps = {
|
||||||
|
children: ReactNode
|
||||||
|
open?: boolean
|
||||||
|
defaultOpen?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sheet({ children, open: controlledOpen, defaultOpen = false, onOpenChange }: SheetProps) {
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)
|
||||||
|
const isControlled = controlledOpen !== undefined
|
||||||
|
const open = isControlled ? controlledOpen : uncontrolledOpen
|
||||||
|
|
||||||
|
const setOpen = (next: boolean) => {
|
||||||
|
if (!isControlled) setUncontrolledOpen(next)
|
||||||
|
onOpenChange?.(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SheetContext.Provider value={{ open, setOpen }}>
|
||||||
|
{children}
|
||||||
|
</SheetContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetTrigger({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
const { setOpen } = useSheet()
|
||||||
|
return (
|
||||||
|
<span className={className} onClick={() => setOpen(true)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetContent({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
const { open, setOpen } = useSheet()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') setOpen(false)
|
||||||
|
}
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}, [open, setOpen])
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sheet-backdrop" onClick={() => setOpen(false)}>
|
||||||
|
<div className={cn('sheet-panel', className)} onClick={e => e.stopPropagation()}>
|
||||||
|
<button
|
||||||
|
className="dialog-close-btn"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
aria-label="Close sheet"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('stack-xs', className)} style={{ marginBottom: 'var(--space-6)' }} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-lg)',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'var(--text-sm)',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex-between', className)}
|
||||||
|
style={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
paddingTop: 'var(--space-4)',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
gap: 'var(--space-3)',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetClose({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
const { setOpen } = useSheet()
|
||||||
|
return (
|
||||||
|
<span className={className} onClick={() => setOpen(false)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
export type SkeletonVariant = 'text' | 'circle' | 'rect'
|
||||||
|
|
||||||
|
type SkeletonProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
variant?: SkeletonVariant
|
||||||
|
width?: string | number
|
||||||
|
height?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Skeleton({ variant = 'text', width, height, className, style, ...props }: SkeletonProps) {
|
||||||
|
const variantClass =
|
||||||
|
variant === 'circle'
|
||||||
|
? 'skeleton-circle'
|
||||||
|
: variant === 'rect'
|
||||||
|
? 'skeleton-rect'
|
||||||
|
: 'skeleton-text'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('skeleton', variantClass, className)}
|
||||||
|
style={{
|
||||||
|
width: width !== undefined ? (typeof width === 'number' ? `${width}px` : width) : undefined,
|
||||||
|
height: height !== undefined ? (typeof height === 'number' ? `${height}px` : height) : undefined,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Card } from './Card'
|
||||||
|
import { Badge } from './Badge'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
type StatCardProps = {
|
||||||
|
title: string
|
||||||
|
value: string | number
|
||||||
|
change?: string
|
||||||
|
trend?: 'up' | 'down' | 'neutral'
|
||||||
|
caption?: string
|
||||||
|
icon?: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatCard({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
change,
|
||||||
|
trend = 'up',
|
||||||
|
caption,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
}: StatCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className={cn('hover-lift', className)}>
|
||||||
|
<div className="flex-between" style={{ marginBottom: 'var(--space-3)' }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 'var(--text-3xs)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
{icon && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: 'var(--primary)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 'var(--text-2xl)',
|
||||||
|
fontWeight: 800,
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
lineHeight: 1.1,
|
||||||
|
letterSpacing: '-0.04em',
|
||||||
|
marginBottom: 'var(--space-2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-between" style={{ marginTop: 'var(--space-2)' }}>
|
||||||
|
{change && (
|
||||||
|
<Badge variant={trend === 'up' ? 'positive' : trend === 'down' ? 'destructive' : 'secondary'}>
|
||||||
|
{trend === 'up' ? '↑' : trend === 'down' ? '↓' : '→'} {change}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{caption && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 'var(--text-3xs)',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{caption}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
type SwitchProps = {
|
||||||
|
checked?: boolean
|
||||||
|
defaultChecked?: boolean
|
||||||
|
onCheckedChange?: (checked: boolean) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
id?: string
|
||||||
|
'aria-label'?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Switch({
|
||||||
|
checked: controlledChecked,
|
||||||
|
defaultChecked = false,
|
||||||
|
onCheckedChange,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
}: SwitchProps) {
|
||||||
|
const [uncontrolledChecked, setUncontrolledChecked] = useState(defaultChecked)
|
||||||
|
const isControlled = controlledChecked !== undefined
|
||||||
|
const checked = isControlled ? controlledChecked : uncontrolledChecked
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (disabled) return
|
||||||
|
const next = !checked
|
||||||
|
if (!isControlled) setUncontrolledChecked(next)
|
||||||
|
onCheckedChange?.(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
id={id}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn('switch-root', checked && 'checked', className)}
|
||||||
|
onClick={toggle}
|
||||||
|
style={{ opacity: disabled ? 0.5 : 1, cursor: disabled ? 'not-allowed' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<span className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
export function Table({ className, ...props }: TableHTMLAttributes<HTMLTableElement>) {
|
||||||
|
return (
|
||||||
|
<div className="ui-table-container">
|
||||||
|
<table className={cn('ui-table', className)} {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableHeader({ className, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
|
||||||
|
return <thead className={cn(className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableBody({ className, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
|
||||||
|
return <tbody className={cn(className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableFooter({ className, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
|
||||||
|
return <tfoot className={cn(className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableRow({ className, ...props }: HTMLAttributes<HTMLTableRowElement>) {
|
||||||
|
return <tr className={cn(className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableHead({ className, ...props }: ThHTMLAttributes<HTMLTableCellElement>) {
|
||||||
|
return <th className={cn(className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableCell({ className, ...props }: TdHTMLAttributes<HTMLTableCellElement>) {
|
||||||
|
return <td className={cn(className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableCaption({ className, ...props }: HTMLAttributes<HTMLTableCaptionElement>) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
className={cn(className)}
|
||||||
|
style={{
|
||||||
|
marginTop: 'var(--space-4)',
|
||||||
|
fontSize: 'var(--text-3xs)',
|
||||||
|
color: 'var(--muted-foreground)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { createContext, useContext, useState, type ReactNode, type HTMLAttributes } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
type TabsContextType = {
|
||||||
|
value: string
|
||||||
|
setValue: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContext = createContext<TabsContextType | null>(null)
|
||||||
|
|
||||||
|
export function useTabs() {
|
||||||
|
const ctx = useContext(TabsContext)
|
||||||
|
if (!ctx) throw new Error('Tabs components must be used within <Tabs>')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabsProps = {
|
||||||
|
defaultValue?: string
|
||||||
|
value?: string
|
||||||
|
onValueChange?: (value: string) => void
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tabs({ defaultValue = '', value: controlledValue, onValueChange, children, className }: TabsProps) {
|
||||||
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue)
|
||||||
|
const isControlled = controlledValue !== undefined
|
||||||
|
const value = isControlled ? controlledValue : uncontrolledValue
|
||||||
|
|
||||||
|
const setValue = (next: string) => {
|
||||||
|
if (!isControlled) setUncontrolledValue(next)
|
||||||
|
onValueChange?.(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContext.Provider value={{ value, setValue }}>
|
||||||
|
<div className={cn('ui-tabs-root', className)}>{children}</div>
|
||||||
|
</TabsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsList({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('ui-tabs-list', className)} role="tablist" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabsTriggerProps = HTMLAttributes<HTMLButtonElement> & {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsTrigger({ value, className, children, ...props }: TabsTriggerProps) {
|
||||||
|
const { value: selectedValue, setValue } = useTabs()
|
||||||
|
const isActive = selectedValue === value
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={isActive}
|
||||||
|
className={cn('ui-tabs-trigger', isActive && 'active', className)}
|
||||||
|
onClick={() => setValue(value)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabsContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsContent({ value, className, children, ...props }: TabsContentProps) {
|
||||||
|
const { value: selectedValue } = useTabs()
|
||||||
|
if (selectedValue !== value) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('ui-tabs-content', className)} role="tabpanel" {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { cn } from '../../utils/cn'
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
|
content: ReactNode
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tooltip({ content, children, className }: TooltipProps) {
|
||||||
|
return (
|
||||||
|
<span className={cn('tooltip-wrapper', className)} tabIndex={0}>
|
||||||
|
{children}
|
||||||
|
<span className="tooltip-box" role="tooltip">
|
||||||
|
{content}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export * from './Badge'
|
||||||
|
export * from './Card'
|
||||||
|
export * from './StatCard'
|
||||||
|
export * from './Kbd'
|
||||||
|
export * from './Dialog'
|
||||||
|
export * from './Alert'
|
||||||
|
export * from './EmptyState'
|
||||||
|
export * from './Tabs'
|
||||||
|
export * from './Progress'
|
||||||
|
export * from './Switch'
|
||||||
|
export * from './Skeleton'
|
||||||
|
export * from './Table'
|
||||||
|
export * from './Sheet'
|
||||||
|
export * from './Tooltip'
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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>`,
|
||||||
|
},
|
||||||
|
]
|
||||||
+18
-6
@@ -6,12 +6,20 @@ export const navigationGroups: NavigationGroup[] = [
|
|||||||
{ label: 'Start here', items: [{ label: 'Overview' }, { label: 'Principles' }, { label: 'Quickstart' }] },
|
{ label: 'Start here', items: [{ label: 'Overview' }, { label: 'Principles' }, { label: 'Quickstart' }] },
|
||||||
{ label: 'Foundations', items: [{ label: 'Color & theme' }, { label: 'Typography' }, { label: 'Spacing' }, { label: 'Elevation' }] },
|
{ label: 'Foundations', items: [{ label: 'Color & theme' }, { label: 'Typography' }, { label: 'Spacing' }, { label: 'Elevation' }] },
|
||||||
{ label: 'Components', items: [{ label: 'Buttons', count: 12 }, { label: 'Form controls' }, { label: 'Badges' }, { label: 'Cards' }, { label: 'Data table' }] },
|
{ label: 'Components', items: [{ label: 'Buttons', count: 12 }, { label: 'Form controls' }, { label: 'Badges' }, { label: 'Cards' }, { label: 'Data table' }] },
|
||||||
{ label: 'Patterns', items: [{ label: 'Command palette' }, { label: 'Empty states' }, { label: 'Settings layouts' }] },
|
{ label: 'AI Workflow', items: [{ label: 'Smart Utils' }, { label: 'Prompt Recipes' }, { label: 'Copilot Rules' }] },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const buttonTabs = ['Preview', 'API reference', 'Usage']
|
export const buttonTabs = ['Preview', 'API reference', 'Usage']
|
||||||
|
|
||||||
export const pageSections = ['Buttons', 'Color tokens', 'Form controls', 'Markdown rendering', 'CodeMirror theme']
|
export const pageSections = [
|
||||||
|
'AI Workflow & Smart Utils',
|
||||||
|
'Buttons',
|
||||||
|
'Color tokens',
|
||||||
|
'Form controls',
|
||||||
|
'Composable by default',
|
||||||
|
'Markdown rendering',
|
||||||
|
'CodeMirror theme',
|
||||||
|
]
|
||||||
|
|
||||||
export const buttonExamples: ButtonExample[] = [
|
export const buttonExamples: ButtonExample[] = [
|
||||||
{ name: 'Primary', label: 'Deploy changes', variant: 'primary' },
|
{ name: 'Primary', label: 'Deploy changes', variant: 'primary' },
|
||||||
@@ -21,8 +29,12 @@ export const buttonExamples: ButtonExample[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const colorTokens: Token[] = [
|
export const colorTokens: Token[] = [
|
||||||
{ name: 'Accent', color: 'var(--accent)', value: '#9b8cff' },
|
{ name: 'Primary', color: 'var(--primary)', value: 'lab(89.9% -47.9 -15.7)' },
|
||||||
{ name: 'Positive', color: 'var(--positive)', value: '#73dfae' },
|
{ name: 'Background', color: 'var(--background)', value: 'lab(0% 0 0)' },
|
||||||
{ name: 'Warning', color: 'var(--warning)', value: '#ffc777' },
|
{ name: 'Card', color: 'var(--card)', value: 'lab(0.9% 0 0)' },
|
||||||
{ name: 'Critical', color: 'var(--critical)', value: '#ff8585' },
|
{ name: 'Destructive', color: 'var(--destructive)', value: 'lab(54.6% 71.7 50.9)' },
|
||||||
|
{ name: 'Chart 1', color: 'var(--chart-1)', value: 'lab(89.9% -47.9 -15.7)' },
|
||||||
|
{ name: 'Chart 2', color: 'var(--chart-2)', value: 'lab(67.0% -58.3 19.5)' },
|
||||||
|
{ name: 'Chart 3', color: 'var(--chart-3)', value: 'lab(72.7% 31.9 97.9)' },
|
||||||
|
{ name: 'Chart 4', color: 'var(--chart-4)', value: 'lab(52.0% 66.1 -78.2)' },
|
||||||
]
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&display=swap');
|
||||||
@import './tokens.css';
|
@import './tokens.css';
|
||||||
|
@import './utils.css';
|
||||||
:root { font-family:var(--font-sans); color:var(--text); background:var(--bg); font-synthesis:none; text-rendering:optimizeLegibility; -webkit-font-smoothing:antialiased; }
|
:root { font-family:var(--font-sans); color:var(--text); background:var(--bg); font-synthesis:none; text-rendering:optimizeLegibility; -webkit-font-smoothing:antialiased; }
|
||||||
* { box-sizing:border-box; } html { scroll-behavior:smooth; } body { margin:0; min-width:320px; } button,input { font:inherit; } button { cursor:pointer; }
|
* { box-sizing:border-box; } html { scroll-behavior:smooth; } body { margin:0; min-width:320px; } button,input { font:inherit; } button { cursor:pointer; }
|
||||||
+170
-100
@@ -1,22 +1,20 @@
|
|||||||
/* ─────────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────────
|
||||||
Northstar design tokens — single source of truth.
|
Northstar Design System — Single Source of Truth Tokens.
|
||||||
|
|
||||||
App.css (and every future stylesheet) consumes ONLY these
|
Modern, high-velocity design system configured for AI-assisted
|
||||||
variables. Change a token here and the whole UI follows.
|
workflows and rapid iteration with strict uniformity.
|
||||||
|
|
||||||
Key one-liners:
|
Tokens utilize CSS Color 4 lab(...) space:
|
||||||
• global text density ......... --root-font-size (e.g. 112%)
|
• Dark: Pitch black, deep cards, radiant electric mint primary
|
||||||
• "small text a bit larger" ... bump --text-xs / --text-2xs
|
• Light: Pure white daylight, crisp charcoal primary, high contrast
|
||||||
• new color ................... add a token, never a hex
|
|
||||||
• accent washes ............... color-mix(in srgb, var(--accent) N%, transparent)
|
|
||||||
───────────────────────────────────────────────────────────── */
|
───────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* ── Global scale, typography & spacing ───────────────────────── */
|
||||||
:root {
|
:root {
|
||||||
/* Global amplifier: every token-driven font size scales from
|
/* Global amplifier: scales every rem-based font size & spacing */
|
||||||
the root size. 100% = 16px. Readability preset: 112%. */
|
|
||||||
--root-font-size: 112%;
|
--root-font-size: 112%;
|
||||||
|
|
||||||
/* ── Type scale (rem-based, snaps to the 9→28px grid) ───── */
|
/* ── Type scale (rem-based, 9→28px grid) ── */
|
||||||
--text-3xs: 0.5625rem; /* 9px micro labels */
|
--text-3xs: 0.5625rem; /* 9px micro labels */
|
||||||
--text-2xs: 0.625rem; /* 10px eyebrows, meta */
|
--text-2xs: 0.625rem; /* 10px eyebrows, meta */
|
||||||
--text-xs: 0.6875rem; /* 11px buttons, nav */
|
--text-xs: 0.6875rem; /* 11px buttons, nav */
|
||||||
@@ -28,102 +26,174 @@
|
|||||||
--text-2xl: 1.75rem; /* 28px section titles */
|
--text-2xl: 1.75rem; /* 28px section titles */
|
||||||
--text-hero: clamp(2.625rem, 5vw, 4.375rem); /* 42→70px display */
|
--text-hero: clamp(2.625rem, 5vw, 4.375rem); /* 42→70px display */
|
||||||
|
|
||||||
/* ── Type families ──────────────────────────────────────── */
|
/* ── Type families ────────────────────────── */
|
||||||
--font-sans: 'Manrope', sans-serif;
|
--font-sans: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
--font-mono: 'DM Mono', monospace;
|
--font-mono: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
|
||||||
/* ── Surfaces ───────────────────────────────────────────── */
|
/* ── Spacing (4px rhythm) ─────────────────── */
|
||||||
--bg: #0b0b10; /* page base */
|
--space-1: 0.25rem; /* 4px */
|
||||||
--bg-deep: #0d0d13; /* code blocks, editor, sidebar wash */
|
--space-2: 0.5rem; /* 8px */
|
||||||
--panel: #111118; /* cards */
|
--space-3: 0.75rem; /* 12px */
|
||||||
--panel-2: #16161f; /* inputs, tabs */
|
--space-4: 1rem; /* 16px */
|
||||||
--panel-3: #1b1b25; /* raised panels, hovers */
|
--space-5: 1.25rem; /* 20px */
|
||||||
--panel-hover: #191923; /* interactive surfaces on hover */
|
--space-6: 1.5rem; /* 24px */
|
||||||
--panel-active: #252535;/* pressed / selected surfaces */
|
--space-7: 2rem; /* 32px */
|
||||||
|
--space-8: 2.5rem; /* 40px */
|
||||||
|
--space-9: 3rem; /* 48px */
|
||||||
|
--space-10: 4rem; /* 64px */
|
||||||
|
--space-11: 5rem; /* 80px */
|
||||||
|
|
||||||
/* ── Lines ──────────────────────────────────────────────── */
|
/* ── Radii ────────────────────────────────── */
|
||||||
--line: #292933; /* default border */
|
--radius-sm: 5px;
|
||||||
--line-soft: #202029; /* hairlines inside dense grids */
|
--radius-md: 6px;
|
||||||
--line-hover: #4a4760; /* border on hover / accent-tinted */
|
--radius-lg: 9px;
|
||||||
--line-focus: #5b5486; /* focused inputs, search */
|
--radius-xl: 12px;
|
||||||
|
--radius-2xl: 14px;
|
||||||
|
--radius-pill: 9999px;
|
||||||
|
|
||||||
/* ── Text ───────────────────────────────────────────────── */
|
/* ── Focus ────────────────────────────────── */
|
||||||
--text: #f1f0f7; /* primary */
|
--focus-ring: 2px solid var(--ring);
|
||||||
--text-bright: #f5f3ff; /* active nav, tab-selected */
|
--focus-offset: 2px;
|
||||||
--text-soft: #b8b6c4; /* longform body */
|
|
||||||
--text-code: #c8c5d2; /* code blocks */
|
|
||||||
--muted: #92919e; /* secondary */
|
|
||||||
--subtle: #666572; /* tertiary, disabled */
|
|
||||||
|
|
||||||
/* ── Accents & status ───────────────────────────────────── */
|
|
||||||
--accent: #a69bff;
|
|
||||||
--accent-contrast: #171526; /* text/icons placed on accent */
|
|
||||||
--positive: #73dfae;
|
|
||||||
--warning: #ffc777;
|
|
||||||
--critical: #ff8585;
|
|
||||||
--pink: #e58bff;
|
|
||||||
--blue: #9bcbff;
|
|
||||||
|
|
||||||
/* ── Spacing (4px rhythm; 1 = 4px … 11 = 80px) ──────────── */
|
|
||||||
--space-1: 0.25rem;
|
|
||||||
--space-2: 0.5rem;
|
|
||||||
--space-3: 0.75rem;
|
|
||||||
--space-4: 1rem;
|
|
||||||
--space-5: 1.25rem;
|
|
||||||
--space-6: 1.5rem;
|
|
||||||
--space-7: 2rem;
|
|
||||||
--space-8: 2.5rem;
|
|
||||||
--space-9: 3rem;
|
|
||||||
--space-10: 4rem;
|
|
||||||
--space-11: 5rem;
|
|
||||||
|
|
||||||
/* ── Radii ──────────────────────────────────────────────── */
|
|
||||||
--radius-sm: 5px; /* kbd, inputs */
|
|
||||||
--radius-md: 6px; /* buttons, nav items */
|
|
||||||
--radius-lg: 9px; /* cards, showcases */
|
|
||||||
--radius-xl: 12px; /* brand mark, orbit core */
|
|
||||||
--radius-2xl: 14px; /* callouts */
|
|
||||||
--radius-pill: 20px; /* toggles */
|
|
||||||
|
|
||||||
/* ── Focus ──────────────────────────────────────────────── */
|
|
||||||
--focus-ring: 2px solid var(--accent);
|
|
||||||
--focus-offset: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Light theme ──────────────────────────────────────────────
|
/* ── Light Palette (Exact lab() Specification) ───────────────── */
|
||||||
Same token names, daylight values. Components never know the
|
:root,
|
||||||
difference — they only ever read var(--*). Toggle via
|
:root[data-theme='light'],
|
||||||
document.documentElement.dataset.theme = 'light' (Topbar does
|
.light {
|
||||||
this and persists to localStorage under 'ns-theme'). */
|
color-scheme: light;
|
||||||
:root[data-theme='light'] {
|
|
||||||
--bg: #f6f5fb;
|
|
||||||
--bg-deep: #efedf6;
|
|
||||||
--panel: #ffffff;
|
|
||||||
--panel-2: #f3f1f9;
|
|
||||||
--panel-3: #eae7f4;
|
|
||||||
--panel-hover: #e7e4f1;
|
|
||||||
--panel-active: #dedbf0;
|
|
||||||
|
|
||||||
--line: #d8d5e7;
|
--background: lab(100% 0 0);
|
||||||
--line-soft: #e4e2ee;
|
--foreground: lab(2.75381% 0 0);
|
||||||
--line-hover: #b3abda;
|
--card: lab(97.68% -.0000298023 .0000119209);
|
||||||
--line-focus: #8d82cf;
|
--card-foreground: lab(2.75381% 0 0);
|
||||||
|
--popover: lab(100% 0 0);
|
||||||
|
--popover-foreground: lab(2.75381% 0 0);
|
||||||
|
--primary: lab(7.78201% -.0000149012 0);
|
||||||
|
--primary-foreground: lab(98.26% 0 0);
|
||||||
|
--secondary: lab(94.2% 0 0);
|
||||||
|
--secondary-foreground: lab(7.78201% -.0000149012 0);
|
||||||
|
--muted: lab(94.2% 0 0);
|
||||||
|
--muted-foreground: lab(39.68% 0 0);
|
||||||
|
--accent: lab(94.2% 0 0);
|
||||||
|
--accent-foreground: lab(7.78201% -.0000149012 0);
|
||||||
|
--destructive: lab(48.4493% 77.4328 61.5452);
|
||||||
|
--destructive-foreground: lab(48.4493% 77.4328 61.5452);
|
||||||
|
--border: lab(86.08% -.0000298023 0);
|
||||||
|
--input: lab(86.08% -.0000298023 0);
|
||||||
|
--ring: lab(42% 0 0);
|
||||||
|
--chart-1: lab(57.1026% 64.2584 89.8886);
|
||||||
|
--chart-2: lab(55.0223% -41.0774 -3.90277);
|
||||||
|
--chart-3: lab(30.372% -13.1853 -18.7887);
|
||||||
|
--chart-4: lab(80.1641% 16.6016 99.2089);
|
||||||
|
--chart-5: lab(72.7183% 31.8672 97.9407);
|
||||||
|
--sidebar: lab(96.52% -.0000298023 .0000119209);
|
||||||
|
--sidebar-foreground: lab(2.75381% 0 0);
|
||||||
|
--sidebar-primary: lab(7.78201% -.0000149012 0);
|
||||||
|
--sidebar-primary-foreground: lab(98.26% 0 0);
|
||||||
|
--sidebar-accent: lab(94.2% 0 0);
|
||||||
|
--sidebar-accent-foreground: lab(7.78201% -.0000149012 0);
|
||||||
|
--sidebar-border: lab(86.08% -.0000298023 0);
|
||||||
|
--sidebar-ring: lab(42% 0 0);
|
||||||
|
--dot-color: lab(7.78201% -.0000149012 0 / .05);
|
||||||
|
|
||||||
--text: #1e1a30;
|
/* ── Legacy Northstar mappings for light theme ── */
|
||||||
--text-bright: #141126;
|
--bg: var(--background);
|
||||||
--text-soft: #4b4665;
|
--bg-deep: var(--sidebar);
|
||||||
--text-code: #383354;
|
--panel: var(--card);
|
||||||
--muted: #625d7d;
|
--panel-2: var(--secondary);
|
||||||
--subtle: #8d89a3;
|
--panel-3: color-mix(in srgb, var(--card) 85%, var(--border));
|
||||||
|
--panel-hover: color-mix(in srgb, var(--card) 70%, var(--border));
|
||||||
|
--panel-active: color-mix(in srgb, var(--secondary) 85%, var(--border));
|
||||||
|
|
||||||
/* Deeper accent + saturated status colors for light-surface contrast */
|
--line: var(--border);
|
||||||
--accent: #6b46e5;
|
--line-soft: var(--sidebar-border);
|
||||||
--accent-contrast: #ffffff;
|
--line-hover: var(--ring);
|
||||||
--positive: #178a5c;
|
--line-focus: var(--ring);
|
||||||
--warning: #a05f00;
|
|
||||||
--critical: #d14141;
|
--text: var(--foreground);
|
||||||
--pink: #bb2fc0;
|
--text-bright: var(--foreground);
|
||||||
--blue: #2f66c9;
|
--text-soft: var(--muted-foreground);
|
||||||
|
--text-code: var(--foreground);
|
||||||
|
--subtle: color-mix(in srgb, var(--muted-foreground) 75%, transparent);
|
||||||
|
|
||||||
|
--accent-brand: var(--primary);
|
||||||
|
--accent-contrast: var(--primary-foreground);
|
||||||
|
--positive: var(--chart-2);
|
||||||
|
--warning: var(--chart-3);
|
||||||
|
--critical: var(--destructive);
|
||||||
|
--pink: var(--chart-5);
|
||||||
|
--blue: var(--chart-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
html { font-size: var(--root-font-size); }
|
/* ── Dark Palette (Exact lab() Specification) ────────────────── */
|
||||||
|
:root:not([data-theme='light']),
|
||||||
|
:root[data-theme='dark'],
|
||||||
|
.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--background: lab(0% 0 0);
|
||||||
|
--foreground: lab(94.78% 0 0);
|
||||||
|
--card: lab(.903296% 0 0);
|
||||||
|
--card-foreground: lab(94.78% 0 0);
|
||||||
|
--popover: lab(.903296% 0 0);
|
||||||
|
--popover-foreground: lab(94.78% 0 0);
|
||||||
|
--primary: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
--primary-foreground: lab(0% 0 0);
|
||||||
|
--secondary: lab(2.47865% 0 0);
|
||||||
|
--secondary-foreground: lab(94.78% 0 0);
|
||||||
|
--muted: lab(2.47865% 0 0);
|
||||||
|
--muted-foreground: lab(60.56% 0 0);
|
||||||
|
--accent: lab(1.98454% 0 0);
|
||||||
|
--accent-foreground: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
--destructive: lab(54.5811% 71.684 50.9279);
|
||||||
|
--destructive-foreground: lab(54.5811% 71.684 50.9279);
|
||||||
|
--border: lab(4.8411% 0 0);
|
||||||
|
--input: lab(1.98454% 0 0);
|
||||||
|
--ring: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
--chart-1: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
--chart-2: lab(66.9756% -58.27 19.5419);
|
||||||
|
--chart-3: lab(72.7183% 31.8672 97.9407);
|
||||||
|
--chart-4: lab(52.0183% 66.11 -78.2316);
|
||||||
|
--chart-5: lab(56.101% 79.4328 31.4532);
|
||||||
|
--sidebar: lab(.498049% 0 0);
|
||||||
|
--sidebar-foreground: lab(94.78% 0 0);
|
||||||
|
--sidebar-primary: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
--sidebar-primary-foreground: lab(0% 0 0);
|
||||||
|
--sidebar-accent: lab(1.98454% 0 0);
|
||||||
|
--sidebar-accent-foreground: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
--sidebar-border: lab(4.8411% 0 0);
|
||||||
|
--sidebar-ring: lab(89.9321% -47.9163 -15.7303);
|
||||||
|
--dot-color: lab(89.9321% -47.9163 -15.7303 / .045);
|
||||||
|
|
||||||
|
/* ── Legacy Northstar mappings for dark theme ── */
|
||||||
|
--bg: var(--background);
|
||||||
|
--bg-deep: var(--sidebar);
|
||||||
|
--panel: var(--card);
|
||||||
|
--panel-2: var(--secondary);
|
||||||
|
--panel-3: color-mix(in srgb, var(--card) 85%, var(--border));
|
||||||
|
--panel-hover: color-mix(in srgb, var(--card) 60%, var(--border));
|
||||||
|
--panel-active: color-mix(in srgb, var(--secondary) 75%, var(--border));
|
||||||
|
|
||||||
|
--line: var(--border);
|
||||||
|
--line-soft: var(--sidebar-border);
|
||||||
|
--line-hover: color-mix(in srgb, var(--ring) 50%, var(--border));
|
||||||
|
--line-focus: var(--ring);
|
||||||
|
|
||||||
|
--text: var(--foreground);
|
||||||
|
--text-bright: var(--foreground);
|
||||||
|
--text-soft: var(--muted-foreground);
|
||||||
|
--text-code: var(--foreground);
|
||||||
|
--subtle: color-mix(in srgb, var(--muted-foreground) 65%, transparent);
|
||||||
|
|
||||||
|
--accent-brand: var(--primary);
|
||||||
|
--accent-contrast: var(--primary-foreground);
|
||||||
|
--positive: var(--chart-2);
|
||||||
|
--warning: var(--chart-3);
|
||||||
|
--critical: var(--destructive);
|
||||||
|
--pink: var(--chart-5);
|
||||||
|
--blue: var(--chart-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: var(--root-font-size);
|
||||||
|
}
|
||||||
+533
@@ -0,0 +1,533 @@
|
|||||||
|
/* ─────────────────────────────────────────────────────────────
|
||||||
|
Northstar Design System — Smart AI Utilities & Primitives.
|
||||||
|
Consumes ONLY tokens. Strictly compliant with token contract.
|
||||||
|
───────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* ── Dot Canvas & Background Grids ───────────────────────────── */
|
||||||
|
.dot-canvas {
|
||||||
|
background-image: radial-gradient(var(--dot-color) 1px, transparent 1px);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-canvas-dense {
|
||||||
|
background-image: radial-gradient(var(--dot-color) 1px, transparent 1px);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Layout Helpers ─────────────────────────────────────────── */
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-xs { display: flex; flex-direction: column; gap: var(--space-1); }
|
||||||
|
.stack-sm { display: flex; flex-direction: column; gap: var(--space-2); }
|
||||||
|
.stack-md { display: flex; flex-direction: column; gap: var(--space-4); }
|
||||||
|
.stack-lg { display: flex; flex-direction: column; gap: var(--space-6); }
|
||||||
|
|
||||||
|
.row-xs { display: flex; align-items: center; gap: var(--space-1); }
|
||||||
|
.row-sm { display: flex; align-items: center; gap: var(--space-2); }
|
||||||
|
.row-md { display: flex; align-items: center; gap: var(--space-4); }
|
||||||
|
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-4 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.grid-2, .grid-3, .grid-4 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Semantic Surface Containers ────────────────────────────── */
|
||||||
|
.surface-card {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--card-foreground);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-5);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-sidebar {
|
||||||
|
background: var(--sidebar);
|
||||||
|
color: var(--sidebar-foreground);
|
||||||
|
border-right: 1px solid var(--sidebar-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-popover {
|
||||||
|
background: var(--popover);
|
||||||
|
color: var(--popover-foreground);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-muted {
|
||||||
|
background: var(--secondary);
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Smart Badges & Status Pills ────────────────────────────── */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-3xs);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary {
|
||||||
|
background: color-mix(in srgb, var(--primary) 14%, transparent);
|
||||||
|
color: var(--primary);
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary {
|
||||||
|
background: var(--secondary);
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-destructive {
|
||||||
|
background: color-mix(in srgb, var(--destructive) 14%, transparent);
|
||||||
|
color: var(--destructive);
|
||||||
|
border-color: color-mix(in srgb, var(--destructive) 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-positive {
|
||||||
|
background: color-mix(in srgb, var(--chart-2) 14%, transparent);
|
||||||
|
color: var(--chart-2);
|
||||||
|
border-color: color-mix(in srgb, var(--chart-2) 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Interactive Elements & Utilities ───────────────────────── */
|
||||||
|
.hover-lift {
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--ring);
|
||||||
|
box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 10%, rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbd-tag {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-3xs);
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
background: var(--secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-pulse {
|
||||||
|
background: color-mix(in srgb, var(--foreground) 7%, var(--card));
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
animation: skeleton-shimmer 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton-shimmer {
|
||||||
|
0%, 100% { opacity: 0.45; }
|
||||||
|
50% { opacity: 0.85; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-primary {
|
||||||
|
box-shadow: 0 0 24px color-mix(in srgb, var(--primary) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Composable Dialog / Modal ──────────────────────────────── */
|
||||||
|
.dialog-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.72);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 50;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: var(--space-4);
|
||||||
|
animation: dialog-fade-in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-panel {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--card-foreground);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
max-width: 520px;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-6);
|
||||||
|
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4), 0 0 0 1px color-mix(in srgb, var(--primary) 12%, transparent);
|
||||||
|
animation: dialog-scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-scale-in {
|
||||||
|
from { opacity: 0; transform: scale(0.96) translateY(8px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-4);
|
||||||
|
right: var(--space-4);
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-1);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-close-btn:hover {
|
||||||
|
background: var(--secondary);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Composable Alert / Callout ─────────────────────────────── */
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-4);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--foreground);
|
||||||
|
position: relative;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: color-mix(in srgb, var(--primary) 6%, var(--card));
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 24%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info .alert-icon {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-positive {
|
||||||
|
background: color-mix(in srgb, var(--chart-2) 6%, var(--card));
|
||||||
|
border-color: color-mix(in srgb, var(--chart-2) 24%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-positive .alert-icon {
|
||||||
|
color: var(--chart-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: color-mix(in srgb, var(--chart-3) 6%, var(--card));
|
||||||
|
border-color: color-mix(in srgb, var(--chart-3) 24%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning .alert-icon {
|
||||||
|
color: var(--chart-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-destructive {
|
||||||
|
background: color-mix(in srgb, var(--destructive) 8%, var(--card));
|
||||||
|
border-color: color-mix(in srgb, var(--destructive) 28%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-destructive .alert-icon {
|
||||||
|
color: var(--destructive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Composable Tabs ────────────────────────────────────────── */
|
||||||
|
.ui-tabs-list {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs-trigger {
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs-trigger:hover {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs-trigger.active {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--foreground);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs-content {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Empty State ────────────────────────────────────────────── */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-10) var(--space-6);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
background: color-mix(in srgb, var(--secondary) 50%, var(--card));
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: color-mix(in srgb, var(--primary) 12%, var(--secondary));
|
||||||
|
color: var(--primary);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress Bar ───────────────────────────────────────────── */
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 0 10px color-mix(in srgb, var(--primary) 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-indeterminate {
|
||||||
|
width: 40%;
|
||||||
|
position: absolute;
|
||||||
|
animation: progress-slide 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-slide {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(350%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Switch Component ───────────────────────────────────────── */
|
||||||
|
.switch-root {
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: var(--border);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 2px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-root.checked {
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--muted-foreground);
|
||||||
|
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-root.checked .switch-thumb {
|
||||||
|
background: var(--primary-foreground);
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Composable Table ───────────────────────────────────────── */
|
||||||
|
.ui-table-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-table th {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
font: var(--text-3xs) var(--font-mono);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
background: var(--secondary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-table td {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-table tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-table tr:hover td {
|
||||||
|
background: color-mix(in srgb, var(--foreground) 3%, var(--card));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Composable Sheet / Drawer ──────────────────────────────── */
|
||||||
|
.sheet-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
z-index: 50;
|
||||||
|
animation: dialog-fade-in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--card-foreground);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
z-index: 51;
|
||||||
|
padding: var(--space-6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: -10px 0 35px rgba(0, 0, 0, 0.35);
|
||||||
|
animation: sheet-slide-in 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sheet-slide-in {
|
||||||
|
from { transform: translateX(100%); }
|
||||||
|
to { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tooltip ────────────────────────────────────────────────── */
|
||||||
|
.tooltip-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-box {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-6px);
|
||||||
|
background: var(--popover);
|
||||||
|
color: var(--popover-foreground);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
font: var(--text-3xs) var(--font-mono);
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 40;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-wrapper:hover .tooltip-box,
|
||||||
|
.tooltip-wrapper:focus-within .tooltip-box {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Skeleton Shapes ────────────────────────────────────────── */
|
||||||
|
.skeleton {
|
||||||
|
background: color-mix(in srgb, var(--foreground) 7%, var(--card));
|
||||||
|
animation: skeleton-shimmer 1.8s ease-in-out infinite;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
height: 14px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-rect {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
type ClassValue = string | number | boolean | undefined | null | { [key: string]: boolean | undefined | null } | ClassValue[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart class merging utility for rapid AI component authoring.
|
||||||
|
* Composes class names conditionally and cleanly flattens nested arrays/objects.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* cn('base-button', variant === 'primary' && 'button-primary', { 'is-active': active })
|
||||||
|
*/
|
||||||
|
export function cn(...inputs: ClassValue[]): string {
|
||||||
|
const classes: string[] = []
|
||||||
|
|
||||||
|
for (const input of inputs) {
|
||||||
|
if (!input) continue
|
||||||
|
|
||||||
|
if (typeof input === 'string' || typeof input === 'number') {
|
||||||
|
classes.push(String(input))
|
||||||
|
} else if (Array.isArray(input)) {
|
||||||
|
const nested = cn(...input)
|
||||||
|
if (nested) classes.push(nested)
|
||||||
|
} else if (typeof input === 'object') {
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
if (value) classes.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
}
|
||||||
Reference in new issue
Block a user