diff --git a/.gitignore b/.gitignore index a547bf3..85c3d68 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.codesearch/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8e0f8cb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +# Northstar Design System — agent handbook + +React 19 + Vite 8 + TypeScript design-system showcase. Dark-first, token-driven UI with a light theme. + +## Commands + +| Task | Command | +|---|---| +| Dev server | `npm run dev` (vite.config.ts pins `0.0.0.0:5173`) | +| Full gate: lint + token contract + typecheck + build | `npm run check` — run before declaring any change done | +| Lint only | `npm run lint` (oxlint) | + +## Sandboxed worker dev flow + +- `vite.config.ts` allows `*.preview.k6n.net`; expose port **5173** via `repo_instance_expose` for a preview URL. +- Removing `allowedHosts` breaks preview ingress with a 403 ("host not allowed"). Don't. + +## 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)`. +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`. +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. +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. +5. "Make the small text a bit larger" = bump `--text-xs`/`--text-2xs`. "Soften a surface" = its token. No grep, no per-component edits. + +## Structure + +``` +src/ + tokens.css ← design tokens + light theme (edit this first) + index.css ← fonts, tokens import, base resets + App.css ← all component styles (token-driven, plain CSS) + App.tsx ← page composition only + components/ ← one component per file, readable multi-line JSX + Topbar.tsx ← owns the theme toggle (useTheme inside) + Hero.tsx, CodeSample.tsx, RightRail.tsx, ButtonMatrix.tsx, + Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx, + MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx, Icon.tsx + data/library.ts ← demo content for the showcase +scripts/check-tokens.mjs ← token contract guard (part of npm run check) +``` + +## Iteration workflow for visual changes + +1. Change the token (or component markup) — never a literal. +2. `npm run check` until green. +3. Verify live: dev server + preview URL; screenshot at desktop width; toggle light/dark via the topbar button. diff --git a/package.json b/package.json index 6126cb9..8be1384 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", + "check": "oxlint && node scripts/check-tokens.mjs && npm run build", "preview": "vite preview" }, "dependencies": { diff --git a/scripts/check-tokens.mjs b/scripts/check-tokens.mjs new file mode 100644 index 0000000..62b7366 --- /dev/null +++ b/scripts/check-tokens.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node +/* Token contract guard — runs as part of `npm run check`. + Bans hardcoded values in application CSS so every visual change + goes through src/tokens.css. */ +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { join } from 'node:path' + +const CSS_ROOTS = ['src'] +const EXEMPT = new Set(['tokens.css']) +// Neutral shine/shadow literals stay allowed (they are surfaces, not palette). +const RGB_ALLOW = [/rgba\(\s*0\s*,\s*0\s*,\s*0/, /rgba\(\s*255\s*,\s*255\s*,\s*255/] + +const violations = [] + +function walk(dir) { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry) + if (statSync(full).isDirectory()) { + walk(full) + } else if (full.endsWith('.css') && !EXEMPT.has(entry)) { + checkFile(full, readFileSync(full, 'utf8')) + } + } +} + +function checkFile(file, css) { + css.split('\n').forEach((line, i) => { + const n = `${file}:${i + 1}` + const hex = line.match(/#[0-9a-fA-F]{3,8}\b/) + if (hex) violations.push(`${n} hex color ${hex[0]} — add a token in tokens.css instead`) + if (/font(-size)?\s*:[^;}]*\d+px/.test(line)) violations.push(`${n} px font size — use var(--text-*)`) + const rgb = line.match(/rgba?\(/) + if (rgb && !RGB_ALLOW.some(re => re.test(line))) { + violations.push(`${n} raw rgba() — use color-mix(in srgb, var(--token) N%, transparent)`) + } + }) +} + +CSS_ROOTS.forEach(walk) + +if (violations.length) { + console.error(`\n✗ token contract violations:\n\n${violations.join('\n')}\n`) + process.exit(1) +} else { + console.log('✓ token contract OK — no hardcoded colors or font sizes found') +} diff --git a/src/App.css b/src/App.css index 4695e47..eae3c92 100644 --- a/src/App.css +++ b/src/App.css @@ -254,7 +254,7 @@ footer{max-width:1180px;margin:0 auto;padding:var(--space-7) var(--space-9) var( .editor-tab{padding:var(--space-3) var(--space-4);color:var(--subtle);font:var(--text-3xs) var(--font-mono);border-right:1px solid var(--line)} .editor-tab.active{color:var(--text-bright);background:var(--bg-deep);border-top:2px solid var(--accent);padding-top:10px} .editor-actions{margin-left:auto;color:var(--subtle);font:var(--text-3xs) var(--font-mono);letter-spacing:3px} -.editor-body{display:grid;grid-template-columns:43px 1fr;min-height:218px;background:linear-gradient(90deg,rgba(20,21,30,.7),transparent 16%)} +.editor-body{display:grid;grid-template-columns:43px 1fr;min-height:218px;background:linear-gradient(90deg,color-mix(in srgb,var(--bg) 70%,transparent),transparent 16%)} .line-numbers{padding:var(--space-4) 10px var(--space-4) 0;border-right:1px solid var(--line-soft);text-align:right;color:var(--subtle);font:var(--text-xs)/1.72 var(--font-mono);user-select:none} .editor-body pre{margin:0;padding:var(--space-4) var(--space-5);font:var(--text-sm)/1.72 var(--font-mono);overflow:auto;color:var(--text-code)} .cm-comment{color:var(--subtle);font-style:italic} @@ -275,7 +275,7 @@ footer{max-width:1180px;margin:0 auto;padding:var(--space-7) var(--space-9) var( /* ── Motion & finishing touches ─────────────────────────────── */ @keyframes rise-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}} -@keyframes soft-glow{0%,100%{box-shadow:0 0 0 rgba(166,155,255,0)}50%{box-shadow:0 0 34px color-mix(in srgb,var(--accent) 16%,transparent)}} +@keyframes soft-glow{0%,100%{box-shadow:0 0 0 transparent}50%{box-shadow:0 0 34px color-mix(in srgb,var(--accent) 16%,transparent)}} @keyframes orbit-drift{from{transform:rotate(-22deg) rotate(0deg)}to{transform:rotate(-22deg) rotate(360deg)}} @keyframes orbit-drift-reverse{from{transform:rotate(58deg) rotate(360deg)}to{transform:rotate(58deg) rotate(0deg)}} @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} diff --git a/src/App.tsx b/src/App.tsx index b35b5c5..c0e7a88 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,45 +1,151 @@ import { useState } from 'react' import './App.css' -import { Icon } from './components/Icon' -import { Sidebar } from './components/Sidebar' import { Topbar } from './components/Topbar' -import { Button } from './components/Button' +import { Sidebar } from './components/Sidebar' +import { Hero } from './components/Hero' import { ButtonMatrix } from './components/ButtonMatrix' -import { CodeEditor } from './components/CodeEditor' -import { FormField, TextInput } from './components/FormField' -import { MarkdownRenderer } from './components/MarkdownRenderer' -import { Section } from './components/Section' import { TokenGrid } from './components/TokenGrid' -import { buttonExamples, buttonTabs, colorTokens, navigationGroups } from './data/library' +import { FormField, TextInput } from './components/FormField' +import { CodeSample } from './components/CodeSample' +import { MarkdownRenderer } from './components/MarkdownRenderer' +import { CodeEditor } from './components/CodeEditor' +import { Section } from './components/Section' +import { RightRail } from './components/RightRail' +import { Icon } from './components/Icon' +import { buttonExamples, buttonTabs, colorTokens, navigationGroups, pageSections } from './data/library' -function App() { +const INSTALL_CMD = 'pnpm add @northstar/ui @northstar/tokens' + +export default function App() { const [active, setActive] = useState('Buttons') const [query, setQuery] = useState('') const [buttonTab, setButtonTab] = useState('Preview') - const [copied, setCopied] = useState(false) const [visible, setVisible] = useState(false) const [menu, setMenu] = useState(false) - const copy = () => { navigator.clipboard?.writeText('pnpm add @northstar/ui @northstar/tokens'); setCopied(true); setTimeout(() => setCopied(false), 1600) } - return
- setMenu(!menu)} /> -
- { setActive(item); setMenu(false) }} /> -
-
Components/{active}
-
Component library

Build with clarity.

A considered set of accessible, composable components for building calm, capable interfaces.

system
ready
TOKENS 48A11Y AAARUNTIME FAST
-
-
Button · 12 variants}>
-
View all tokens }>
-
-
button.tsx
import {'{ Button }'} from '@northstar/ui'{'\n\n'}export function DeployAction() {'{'}{'\n'}  return ({'\n'}    <Button variant="primary">Deploy changes</Button>{'\n'}  ){ '\n'}{'}'}
-
Markdown · styled}>
-
codemirror · dark}>
-
-
northstar design systemmade for the long haul · 2025
-
+ return ( +
+ setMenu(!menu)} /> + +
+ { + setActive(item) + setMenu(false) + }} + /> + +
+
+ Components + / + {active} +
+ + + +
+
+
Button · 12 variants} + > + +
+ +
+ View all tokens + + } + > + +
+ +
+
+ + + + +
+ + +
+
+ +
+
+ +
+ +
+ +
Markdown · styled} + > + +
+ +
codemirror · dark} + > + +
+
+ + +
+ +
+ northstar design system + made for the long haul · 2025 +
+
+
-
+ ) } -export default App +function copyInstall() { + navigator.clipboard?.writeText(INSTALL_CMD) +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx index afb7b56..a56480e 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,9 +1,17 @@ -import type { ReactNode, ButtonHTMLAttributes } from 'react' +import type { ButtonHTMLAttributes, ReactNode } from 'react' -type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' -type ButtonProps = ButtonHTMLAttributes & { variant?: ButtonVariant; children: ReactNode } +export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' + +type ButtonProps = ButtonHTMLAttributes & { + variant?: ButtonVariant + children: ReactNode +} /** Shared action primitive. Visual variants stay in App.css so themes can override them. */ export function Button({ variant = 'secondary', className = '', children, ...props }: ButtonProps) { - return + return ( + + ) } diff --git a/src/components/ButtonMatrix.tsx b/src/components/ButtonMatrix.tsx index 629491f..b35eb2e 100644 --- a/src/components/ButtonMatrix.tsx +++ b/src/components/ButtonMatrix.tsx @@ -1,10 +1,77 @@ import { Button } from './Button' import { Icon } from './Icon' -export type ButtonExample = { name: string; label: string; variant: 'primary' | 'secondary' | 'danger' | 'ghost' } -type ButtonMatrixProps = { examples: ButtonExample[]; tabs: string[]; filter?: string; activeTab?: string; onTabChange?: (tab: string) => void } +export type ButtonExample = { + name: string + label: string + variant: 'primary' | 'secondary' | 'danger' | 'ghost' +} + +type ButtonMatrixProps = { + examples: ButtonExample[] + tabs: string[] + filter?: string + activeTab?: string + onTabChange?: (tab: string) => void +} export function ButtonMatrix({ examples, tabs, filter = '', activeTab, onTabChange }: ButtonMatrixProps) { - const visible = examples.filter(example => `${example.name} ${example.label}`.toLowerCase().includes(filter.toLowerCase())) - return <>
{tabs.map(tab => )}
Interactive statesbutton.tsx
{visible.map(example =>
{example.name}
)}
Density Keyboard ready
+ const visible = examples.filter(example => + `${example.name} ${example.label}`.toLowerCase().includes(filter.toLowerCase()), + ) + + return ( + <> +
+ {tabs.map(tab => ( + + ))} +
+ +
+
+ Interactive states + button.tsx +
+ +
+ {visible.map(example => ( +
+ {example.name} + + + + +
+ ))} +
+ +
+ Density + + + + Keyboard ready + +
+
+ + ) } diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx index 0dc4ef1..6dcb4b2 100644 --- a/src/components/CodeEditor.tsx +++ b/src/components/CodeEditor.tsx @@ -2,6 +2,50 @@ import { Icon } from './Icon' export type CodeEditorProps = { fileName?: string; language?: string } +const LINE_NUMBERS = [1, 2, 3, 4, 5, 6, 7, 8] + export function CodeEditor({ fileName = 'theme.ts', language = 'TypeScript' }: CodeEditorProps) { - return
{fileName}tokens.css● ● ●
1
2
3
4
5
6
7
8
// A calm editor for focused work{'\n'}export const theme = {'{'}{'\n'}  name: 'northstar-dark',{'\n'}  colors: {'{'}{'\n'}    accent: '#a69bff',{'\n'}    keyword: '#e58bff',{'\n'}    comment: '#666572'{'\n'}  {'}'}{'\n'}{'}'}
Ln 4, Col 18{language} · UTF-8 No issues
+ return ( +
+
+ + {fileName} + tokens.css + ● ● ● +
+ +
+
+ {LINE_NUMBERS.map(n => ( + + {n} +
+
+ ))} +
+
+          
+            // A calm editor for focused work
+            {'\n'}
+            export const theme = {'{'}
+            {'\n'}  name: 'northstar-dark',
+            {'\n'}  colors: {'{'}
+            {'\n'}    accent: '#a69bff',
+            {'\n'}    keyword: '#e58bff',
+            {'\n'}    comment: '#666572'
+            {'\n'}  {'}'}
+            {'\n'}{'}'}
+          
+        
+
+ +
+ Ln 4, Col 18 + {language} · UTF-8 + + No issues + +
+
+ ) } diff --git a/src/components/CodeSample.tsx b/src/components/CodeSample.tsx new file mode 100644 index 0000000..ea4d038 --- /dev/null +++ b/src/components/CodeSample.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react' +import { Icon } from './Icon' + +const INSTALL_CMD = 'pnpm add @northstar/ui @northstar/tokens' + +/** Tokenized segments keep the code sample syntax-highlighted without a markdown dependency. */ +type Segment = [className: 'pink' | 'green' | 'blue' | 'plain', text: string] + +const CODE_LINES: Segment[] = [ + ['pink', 'import'], + ['plain', ' { Button } '], + ['pink', 'from'], + ['green', " '@northstar/ui'"], + ['plain', '\n\nexport function '], + ['blue', 'DeployAction'], + ['plain', '() {\n return (\n'], + ['plain', ' \n )\n}'], +] + +export function CodeSample() { + const [copied, setCopied] = useState(false) + + const copy = () => { + navigator.clipboard?.writeText(INSTALL_CMD) + setCopied(true) + setTimeout(() => setCopied(false), 1600) + } + + return ( +
+
+ button.tsx + +
+
+        
+          {CODE_LINES.map(([className, text], i) =>
+            className === 'plain' ? (
+              {text}
+            ) : (
+              
+                {text}
+              
+            ),
+          )}
+        
+      
+
+ ) +} diff --git a/src/components/FormField.tsx b/src/components/FormField.tsx index acefe81..4101b0d 100644 --- a/src/components/FormField.tsx +++ b/src/components/FormField.tsx @@ -1,5 +1,25 @@ import type { InputHTMLAttributes, ReactNode } from 'react' -type FormFieldProps = { label: string; hint: string; children: ReactNode; readOnlyLabel?: string } -export function FormField({ label, hint, children, readOnlyLabel }: FormFieldProps) { return } -export function TextInput(props: InputHTMLAttributes) { return } +type FormFieldProps = { + label: string + hint: string + children: ReactNode + readOnlyLabel?: string +} + +export function FormField({ label, hint, children, readOnlyLabel }: FormFieldProps) { + return ( + + ) +} + +export function TextInput(props: InputHTMLAttributes) { + return +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx new file mode 100644 index 0000000..f7550d2 --- /dev/null +++ b/src/components/Hero.tsx @@ -0,0 +1,41 @@ +import { Button } from './Button' +import { Icon } from './Icon' + +export function Hero() { + return ( +
+
+ Component library +

Build with clarity.

+

A considered set of accessible, composable components for building calm, capable interfaces.

+
+ + +
+
+ +
+ ) +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index e8cf366..8a98478 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,9 +1,31 @@ type IconProps = { name: string; size?: number } const paths: Record = { - search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm6-2 4 4', sun: 'M12 3v2m0 14v2M4 12h2m12 0h2M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z', copy: 'M8 8h10v12H8zM6 16H4V4h12v2', check: 'm5 12 4 4L19 6', eye: 'M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Zm10 3a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', arrow: 'M5 12h14m-6-6 6 6-6 6', menu: 'M4 6h16M4 12h16M4 18h16', layers: 'm12 3 9 5-9 5-9-5 9-5Zm-9 9 9 5 9-5M3 16l9 5 9-5', + search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm6-2 4 4', + sun: 'M12 3v2m0 14v2M4 12h2m12 0h2M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z', + moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z', + copy: 'M8 8h10v12H8zM6 16H4V4h12v2', + check: 'm5 12 4 4L19 6', + eye: 'M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Zm10 3a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', + arrow: 'M5 12h14m-6-6 6 6-6 6', + menu: 'M4 6h16M4 12h16M4 18h16', + layers: 'm12 3 9 5-9 5-9-5 9-5Zm-9 9 9 5 9-5M3 16l9 5 9-5', } export function Icon({ name, size = 18 }: IconProps) { - return + return ( + + ) } diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index cf46d3e..23354b3 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -1,7 +1,60 @@ -import type { ReactNode } from 'react' +export type MarkdownRendererProps = { + title?: string + kicker?: string +} -export type MarkdownRendererProps = { title?: string; kicker?: string; children?: ReactNode } +export function MarkdownRenderer({ + title = 'Quiet power, shipped weekly.', + kicker = 'NORTHSTAR / RELEASE NOTES', +}: MarkdownRendererProps) { + return ( +
+
+ Preview + release-notes.md +
-export function MarkdownRenderer({ title = 'Quiet power, shipped weekly.', kicker = 'NORTHSTAR / RELEASE NOTES' }: MarkdownRendererProps) { - return
Previewrelease-notes.md
{kicker}

{title}

Our components help teams move quickly without making the interface feel rushed.

✦

New in 0.8.2
Button focus rings now respect your theme while keeping a dependable contrast ratio.

What changed

  • Added accent-subtle surface token
  • Improved keyboard navigation in menus
  • Reduced layout shift during loading states
TokenValueRole
surface-1#111118Elevated panels
accent#A69BFFInteractive focus
+
+ {kicker} +

{title}

+

Our components help teams move quickly without making the interface feel rushed.

+ +
+ ✦ +

+ New in 0.8.2 +
+ Button focus rings now respect your theme while keeping a dependable contrast ratio. +

+
+ +

What changed

+
    +
  • + Added accent-subtle surface token +
  • +
  • Improved keyboard navigation in menus
  • +
  • Reduced layout shift during loading states
  • +
+ +
+
+ Token + Value + Role +
+
+ surface-1 + #111118 + Elevated panels +
+
+ accent + #A69BFF + Interactive focus +
+
+
+
+ ) } diff --git a/src/components/RightRail.tsx b/src/components/RightRail.tsx new file mode 100644 index 0000000..0fab884 --- /dev/null +++ b/src/components/RightRail.tsx @@ -0,0 +1,39 @@ +import { Button } from './Button' +import { Icon } from './Icon' + +type RightRailProps = { sections: string[] } + +export function RightRail({ sections }: RightRailProps) { + return ( + + ) +} diff --git a/src/components/Section.tsx b/src/components/Section.tsx index 5b5e1d7..82bb953 100644 --- a/src/components/Section.tsx +++ b/src/components/Section.tsx @@ -1,7 +1,27 @@ import type { ReactNode } from 'react' -type SectionProps = { index: string; title: string; description: string; meta?: ReactNode; children: ReactNode; id?: string; className?: string } +type SectionProps = { + index: string + title: string + description: string + meta?: ReactNode + children: ReactNode + id?: string + className?: string +} export function Section({ index, title, description, meta, children, id, className = '' }: SectionProps) { - return
{index}

{title}

{description}

{meta}
{children}
+ return ( +
+
+
+ {index} +

{title}

+

{description}

+
+ {meta} +
+ {children} +
+ ) } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0e29a81..5ec38e0 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,8 +2,58 @@ import { Icon } from './Icon' export type NavigationItem = { label: string; count?: number } export type NavigationGroup = { label: string; items: NavigationItem[] } -type SidebarProps = { active: string; groups: NavigationGroup[]; onSelect: (item: string) => void } + +type SidebarProps = { + active: string + groups: NavigationGroup[] + onSelect: (item: string) => void +} export function Sidebar({ active, groups, onSelect }: SidebarProps) { - return + return ( + + ) } diff --git a/src/components/TokenGrid.tsx b/src/components/TokenGrid.tsx index ba4cc55..5bf50bc 100644 --- a/src/components/TokenGrid.tsx +++ b/src/components/TokenGrid.tsx @@ -1,8 +1,24 @@ import { Icon } from './Icon' export type Token = { name: string; color: string; value: string } + type TokenGridProps = { tokens: Token[]; onCopy?: (value: string) => void } export function TokenGrid({ tokens, onCopy }: TokenGridProps) { - return
{tokens.map(token =>
{token.name}{token.value}
)}
+ return ( +
+ {tokens.map(token => ( +
+
+
+ {token.name} + {token.value} +
+ +
+ ))} +
+ ) } diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index aeda139..8141e62 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -1,7 +1,61 @@ +import { useState } from 'react' import { Icon } from './Icon' -type TopbarProps = { query: string; onQueryChange: (value: string) => void; onMenuToggle: () => void } +type TopbarProps = { + query: string + onQueryChange: (value: string) => void + onMenuToggle: () => void +} + +/** App theme is a data attribute on ; tokens.css defines both palettes. */ +function useTheme() { + const [theme, setTheme] = useState<'dark' | 'light'>(() => + localStorage.getItem('ns-theme') === 'light' ? 'light' : 'dark', + ) + const toggle = () => { + const next = theme === 'dark' ? 'light' : 'dark' + setTheme(next) + document.documentElement.dataset.theme = next + localStorage.setItem('ns-theme', next) + } + return { theme, toggle } +} export function Topbar({ query, onQueryChange, onMenuToggle }: TopbarProps) { - return
northstarv0.8.2
onQueryChange(e.target.value)} placeholder="Search components, tokens, docs..." />⌘ K
+ const { theme, toggle } = useTheme() + + return ( +
+
+
+ +
+ northstar + v0.8.2 +
+ +
+ + onQueryChange(e.target.value)} + placeholder="Search components, tokens, docs..." + /> + ⌘ K +
+ + + + +
+ ) } diff --git a/src/tokens.css b/src/tokens.css new file mode 100644 index 0000000..ced1d7a --- /dev/null +++ b/src/tokens.css @@ -0,0 +1,129 @@ +/* ───────────────────────────────────────────────────────────── + Northstar design tokens — single source of truth. + + App.css (and every future stylesheet) consumes ONLY these + variables. Change a token here and the whole UI follows. + + Key one-liners: + • global text density ......... --root-font-size (e.g. 112%) + • "small text a bit larger" ... bump --text-xs / --text-2xs + • new color ................... add a token, never a hex + • accent washes ............... color-mix(in srgb, var(--accent) N%, transparent) + ───────────────────────────────────────────────────────────── */ + +:root { + /* Global amplifier: every token-driven font size scales from + the root size. 100% = 16px. Readability preset: 112%. */ + --root-font-size: 112%; + + /* ── Type scale (rem-based, snaps to the 9→28px grid) ───── */ + --text-3xs: 0.5625rem; /* 9px micro labels */ + --text-2xs: 0.625rem; /* 10px eyebrows, meta */ + --text-xs: 0.6875rem; /* 11px buttons, nav */ + --text-sm: 0.75rem; /* 12px inputs, copy */ + --text-md: 0.875rem; /* 14px longform body */ + --text-base: 1rem; /* 16px ledes, brand */ + --text-lg: 1.3125rem; /* 21px card headings */ + --text-xl: 1.5625rem; /* 25px sub-headlines */ + --text-2xl: 1.75rem; /* 28px section titles */ + --text-hero: clamp(2.625rem, 5vw, 4.375rem); /* 42→70px display */ + + /* ── Type families ──────────────────────────────────────── */ + --font-sans: 'Manrope', sans-serif; + --font-mono: 'DM Mono', monospace; + + /* ── Surfaces ───────────────────────────────────────────── */ + --bg: #0b0b10; /* page base */ + --bg-deep: #0d0d13; /* code blocks, editor, sidebar wash */ + --panel: #111118; /* cards */ + --panel-2: #16161f; /* inputs, tabs */ + --panel-3: #1b1b25; /* raised panels, hovers */ + --panel-hover: #191923; /* interactive surfaces on hover */ + --panel-active: #252535;/* pressed / selected surfaces */ + + /* ── Lines ──────────────────────────────────────────────── */ + --line: #292933; /* default border */ + --line-soft: #202029; /* hairlines inside dense grids */ + --line-hover: #4a4760; /* border on hover / accent-tinted */ + --line-focus: #5b5486; /* focused inputs, search */ + + /* ── Text ───────────────────────────────────────────────── */ + --text: #f1f0f7; /* primary */ + --text-bright: #f5f3ff; /* active nav, tab-selected */ + --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 ────────────────────────────────────────────── + Same token names, daylight values. Components never know the + difference — they only ever read var(--*). Toggle via + document.documentElement.dataset.theme = 'light' (Topbar does + this and persists to localStorage under 'ns-theme'). */ +: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; + --line-soft: #e4e2ee; + --line-hover: #b3abda; + --line-focus: #8d82cf; + + --text: #1e1a30; + --text-bright: #141126; + --text-soft: #4b4665; + --text-code: #383354; + --muted: #625d7d; + --subtle: #8d89a3; + + /* Deeper accent + saturated status colors for light-surface contrast */ + --accent: #6b46e5; + --accent-contrast: #ffffff; + --positive: #178a5c; + --warning: #a05f00; + --critical: #d14141; + --pink: #bb2fc0; + --blue: #2f66c9; +} + +html { font-size: var(--root-font-size); }