From 41d4776f03396c826fd65c95c4c43884fb28cf53 Mon Sep 17 00:00:00 2001 From: Thomas Hart Date: Sat, 18 Jul 2026 19:12:19 +0000 Subject: [PATCH] feat: add typed design-token system generating CSS custom properties --- README.md | 10 ++++++ app/globals.css | 18 ++-------- app/layout.tsx | 6 ++++ app/page.tsx | 3 +- app/tokens/page.tsx | 53 ++++++++++++++++++++++++++++ src/components/concept-card.tsx | 2 +- src/tokens/index.ts | 41 ++++++++++++++++++++++ src/tokens/tokens.ts | 33 ++++++++++++++++++ test/tokens.test.ts | 62 +++++++++++++++++++++++++++++++++ 9 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 app/tokens/page.tsx create mode 100644 src/tokens/index.ts create mode 100644 src/tokens/tokens.ts create mode 100644 test/tokens.test.ts diff --git a/README.md b/README.md index 7412103..c721694 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Front-end work has changed a lot since the SPA-everything era: rendering moved b ## What's implemented - Scaffold: Next.js 15 App Router, React 19, strict TypeScript, Vitest + Testing Library, GitHub Actions CI, and a design-token stylesheet. The home route lists the planned concepts using an accessible `ConceptCard` component. +- Design tokens: a typed token tree in `src/tokens` that generates the app's CSS custom properties. The root layout injects the generated `:root` rule at render time, so the token module is the runtime source and there is no second copy to drift. See the `/tokens` route. ## Stack @@ -50,6 +51,15 @@ import { ConceptCard } from '@/components/concept-card' /> ``` +Styles reference tokens through a typed helper, so a bad name fails to compile: + +```tsx +import { token } from '@/tokens' + +
+// token('color', 'nope') -> type error +``` + ## Layout ``` diff --git a/app/globals.css b/app/globals.css index 3cb1f5c..6256ac0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,20 +1,8 @@ +/* Token custom properties (--color-*, --space-*, --radius-*, --font-size-*) are + generated from src/tokens and injected by the root layout. Only values that + are not part of the token tree live here. */ :root { color-scheme: light dark; - - --color-bg: #0f1115; - --color-surface: #171a21; - --color-border: #262b36; - --color-text: #e7eaf0; - --color-muted: #9aa3b2; - --color-accent: #5eead4; - - --space-2: 0.5rem; - --space-3: 0.75rem; - --space-4: 1rem; - --space-6: 1.5rem; - --space-8: 2rem; - - --radius: 12px; --font-sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace; } diff --git a/app/layout.tsx b/app/layout.tsx index 7535867..418d709 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,10 @@ import type { Metadata, Viewport } from 'next' import type { ReactNode } from 'react' +import { cssVariablesToRootRule, tokensToCssVariables } from '@/tokens' import './globals.css' +const tokenRootRule = cssVariablesToRootRule(tokensToCssVariables()) + export const metadata: Metadata = { title: 'Modern Frontend Lab', description: @@ -15,6 +18,9 @@ export const viewport: Viewport = { export default function RootLayout({ children }: { children: ReactNode }) { return ( + +