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 libraryBuild with clarity.
A considered set of accessible, composable components for building calm, capable interfaces.
TOKENS 48A11Y AAARUNTIME FAST
-
-
-
-
-
button.tsx
import {'{ Button }'} from '@northstar/ui'{'\n\n'}export function DeployAction() {'{'}{'\n'} return ({'\n'} <Button variant="primary">Deploy changes</Button>{'\n'} ){ '\n'}{'}'}
-
-
-
-
-
+ return (
+
+
setMenu(!menu)} />
+
+
+
{
+ setActive(item)
+ setMenu(false)
+ }}
+ />
+
+
+
+ Components
+ /
+ {active}
+
+
+
+
+
+
+
+
+
+ View all tokens
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Markdown · styled}
+ >
+
+
+
+
codemirror · dark}
+ >
+
+
+
+
+
+
+
+
+
+
-
+ )
}
-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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ system
+
+ ready
+
+
+
TOKENS 48
+
A11Y AAA
+
RUNTIME FAST
+
+
+ )
+}
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
+ const { theme, toggle } = useTheme()
+
+ return (
+
+ )
}
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); }