import type { Meta, StoryObj } from '@storybook/react-vite'
import tokens from '../../public/tokens.json'
// ─── helpers ────────────────────────────────────────────────────────────────
function resolveAlias(value: string): string {
// e.g. "{color.blue.500}" → tokens.color.blue["500"].$value
const match = value.match(/^\{(.+)\}$/)
if (!match) return value
const parts = match[1].split('.')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let node: any = tokens
for (const p of parts) node = node?.[p]
return node?.['$value'] ?? value
}
function isLight(hex: string): boolean {
const c = hex.replace('#', '')
const r = parseInt(c.slice(0, 2), 16)
const g = parseInt(c.slice(2, 4), 16)
const b = parseInt(c.slice(4, 6), 16)
return (r * 299 + g * 587 + b * 114) / 1000 > 155
}
// ─── sub-components ─────────────────────────────────────────────────────────
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
function SubTitle({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
// ─── Color palette ───────────────────────────────────────────────────────────
// Derive color families and steps dynamically from tokens
const COLOR_FAMILIES = Object.keys(tokens.color).filter(k => k !== 'semantic' && k !== 'black')
const STEPS = Object.keys(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(tokens.color as any)[COLOR_FAMILIES[0]] ?? {}
).sort((a, b) => Number(a) - Number(b))
function ColorPalette() {
return (
Color Palette
{COLOR_FAMILIES.map(family => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const familyTokens = (tokens.color as any)[family]
return (
{family}
{STEPS.map(step => {
const hex: string = familyTokens[step]?.['$value'] ?? '#ccc'
const light = isLight(hex)
return (
{step}
{hex}
)
})}
)
})}
)
}
// ─── Semantic colors ─────────────────────────────────────────────────────────
const SEMANTIC_LABELS: Record = {
accent: 'ACCENT',
positive: 'POSITIVE',
destructive: 'NEGATIVE',
secondary: 'SECONDARY',
standard: 'STANDARD',
}
function SemanticColors() {
return (
Semantic Colors
{Object.entries(tokens.color.semantic).map(([key, token]) => {
const raw = token['$value'] as string
const hex = resolveAlias(raw)
const light = isLight(hex)
// Extract alias path like "blue-500" from "{color.blue.500}"
const aliasMatch = raw.match(/^\{color\.(.+)\}$/)
const aliasLabel = aliasMatch ? aliasMatch[1].replace('.', '-') : raw
return (
{hex}
{SEMANTIC_LABELS[key] ?? key}
{aliasLabel}
)
})}
)
}
// ─── Gradients ───────────────────────────────────────────────────────────────
function resolveGradientStops(stops: Array<{ color: string; position: number }>): string {
return stops
.map(s => `${resolveAlias(s.color)} ${Math.round(s.position * 100)}%`)
.join(', ')
}
function Gradients() {
const gradientEntries = Object.entries(tokens.gradient)
const cssStrings = gradientEntries.map(([, g]) =>
resolveGradientStops(g['$value'] as Array<{ color: string; position: number }>)
)
// Find the bg and overlay gradients for the combined preview
const bgIndex = gradientEntries.findIndex(([k]) => k === 'header-bg')
const overlayIndex = gradientEntries.findIndex(([k]) => k === 'header-overlay')
const bgCss = bgIndex >= 0 ? cssStrings[bgIndex] : cssStrings[0]
const overlayCss = overlayIndex >= 0 ? cssStrings[overlayIndex] : cssStrings[1]
return (
Gradients
{/* Left: individual gradient swatches (bg only) */}
{gradientEntries.map(([key, g], i) =>
key === 'header-overlay' ? null : (
{key}
{g['$description']}
)
)}
{/* Right: combined as seen in ApplicationHeader */}
Header-BG with Overlay
{/* Overlay offset from top by 0.5rem, matching .application-header-gradient::before */}
--gradient-header-overlay
)
}
// ─── Typography ──────────────────────────────────────────────────────────────
function Typography() {
const { 'text-size': textSize } = tokens.typography
return (
Typography
Text Size
{Object.entries(textSize).map(([key, token]) => {
const raw = token['$value']
const isAlias = typeof raw === 'string' && raw.startsWith('{')
const resolvedValue = isAlias
? resolveAlias(raw) as unknown as { value: number; unit: string }
: raw as { value: number; unit: string }
const desc = token['$description'] as string
const sailName = desc.split('—')[0].trim().replace(/^[^.]+\./, '')
// rem values are relative to browser default 16px root
const px = Math.round(resolvedValue.value * 16)
const sizeLabel = isAlias
? `→ text-${raw.replace(/^\{typography\.text-size\./, '').replace(/\}$/, '')} (${resolvedValue.value}rem / ${px}px)`
: `${resolvedValue.value}${resolvedValue.unit} / ${px}px`
return (
text-{key}
The quick brown fox
{sizeLabel}
{sailName}
)
})}
)
}
// ─── Spacing ─────────────────────────────────────────────────────────────────
function Spacing() {
const { margin } = tokens.spacing
return (
Spacing
Margin and Padding Scale
{Object.entries(margin).map(([key, token]) => {
const val = token['$value'] as { value: number; unit: string }
const px = val.unit === 'rem' ? val.value * 16 : val.value
const width = Math.max(px, 4)
const desc = (token['$description'] as string).replace(/^[^.]+\./, '')
return (
{key}
{val.value}{val.unit} ({px}px) — {desc}
)
})}
)
}
// ─── Shape ────────────────────────────────────────────────────────────────────
function Shape() {
const { radius } = tokens.spacing
return (
Shape
Border Radius
{Object.entries(radius).map(([key, token]) => {
const val = token['$value'] as { value: number; unit: string }
const px = val.value * 16
return (
{key}
{val.value}{val.unit} ({px}px)
)
})}
)
}
// ─── Full page ────────────────────────────────────────────────────────────────
function DesignTokensPage() {
return (
Design Tokens
Aurora color palette, semantic mappings, typography, spacing, and gradients — sourced from{' '}
public/tokens.json
)
}
// ─── Story ────────────────────────────────────────────────────────────────────
const meta = {
title: 'Foundation/Design Tokens',
component: DesignTokensPage,
parameters: { layout: 'fullscreen' },
} satisfies Meta
export default meta
type Story = StoryObj
export const AllTokens: Story = {}