Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions corpus/frontend/design-tokens/index.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace: frontend.design-tokens
procedures:
step-1:
file: step-1.yaml
step-2:
file: step-2.yaml
step-3:
file: step-3.yaml
step-4:
file: step-4.yaml
step-5:
file: step-5.yaml
step-6:
file: step-6.yaml
step-7:
file: step-7.yaml
step-8:
file: step-8.yaml
51 changes: 51 additions & 0 deletions corpus/frontend/design-tokens/step-1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
step: 1
title: Define color ramp primitives in @theme
description: >-
Create 11-stop OKLCH ramps for brand, neutral, and pop colors. These are
static compile-time values.
code: |
@theme {
--color-brand-50: oklch(0.98 0.01 291);
--color-brand-100: oklch(0.96 0.02 291);
--color-brand-200: oklch(0.90 0.04 291);
--color-brand-300: oklch(0.82 0.07 291);
--color-brand-400: oklch(0.72 0.11 291);
--color-brand-500: oklch(0.62 0.14 291);
--color-brand-600: oklch(0.54 0.14 291);
--color-brand-700: oklch(0.46 0.13 291);
--color-brand-800: oklch(0.38 0.11 291);
--color-brand-900: oklch(0.28 0.08 291);
--color-brand-950: oklch(0.18 0.05 291);

--color-neutral-50: oklch(0.98 0.008 60);
--color-neutral-100: oklch(0.96 0.008 60);
--color-neutral-200: oklch(0.92 0.008 60);
--color-neutral-300: oklch(0.84 0.008 60);
--color-neutral-400: oklch(0.72 0.008 60);
--color-neutral-500: oklch(0.58 0.010 60);
--color-neutral-600: oklch(0.46 0.010 60);
--color-neutral-700: oklch(0.35 0.010 60);
--color-neutral-800: oklch(0.26 0.010 60);
--color-neutral-900: oklch(0.18 0.008 60);
--color-neutral-950: oklch(0.12 0.005 60);

--color-pop-50: oklch(0.97 0.025 50);
--color-pop-100: oklch(0.94 0.045 50);
--color-pop-200: oklch(0.88 0.075 50);
--color-pop-300: oklch(0.80 0.110 50);
--color-pop-400: oklch(0.72 0.145 50);
--color-pop-500: oklch(0.65 0.165 50);
--color-pop-600: oklch(0.57 0.155 50);
--color-pop-700: oklch(0.48 0.140 50);
--color-pop-800: oklch(0.38 0.110 50);
--color-pop-900: oklch(0.28 0.070 50);
--color-pop-950: oklch(0.18 0.040 50);
}
rules:
- Use @theme (not :root) for primitive ramps - they become Tailwind static utilities
- Hue angle (H) must be consistent across all stops in a ramp
- Chroma (C) peaks around 400-600, decreases toward 50 and 950
- Lightness (L) must be monotonically decreasing from 50 to 950
gotchas:
- "@theme values are static - they cannot reference CSS custom properties or be overridden at runtime by JavaScript."
- "If you change the H value after building components, all color utilities change across the app. Lock the hue angle early."
66 changes: 66 additions & 0 deletions corpus/frontend/design-tokens/step-2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
step: 2
title: Map semantic tokens in :root and .dark
description: >-
Create role-based semantic tokens that reference primitives. These are the
tokens your components actually use.
code: |
:root {
/* Backgrounds */
--color-bg: var(--color-neutral-50);
--color-bg-subtle: var(--color-neutral-100);
--color-surface: #ffffff;
--color-surface-raised: var(--color-neutral-50);

/* Borders */
--color-border: var(--color-neutral-200);
--color-border-strong: var(--color-neutral-300);
--color-border-focus: var(--color-brand-500);

/* Text */
--color-text: var(--color-neutral-900);
--color-text-muted: var(--color-neutral-500);
--color-text-subtle: var(--color-neutral-400);
--color-text-inverted: #ffffff;

/* Interactive */
--color-primary: var(--color-brand-500);
--color-primary-hover: var(--color-brand-600);
--color-primary-fg: #ffffff;
--color-secondary: var(--color-neutral-200);
--color-secondary-hover: var(--color-neutral-300);
--color-secondary-fg: var(--color-neutral-900);

/* Status */
--color-success: oklch(0.55 0.14 145);
--color-success-bg: oklch(0.96 0.04 145);
--color-success-fg: #ffffff;
--color-error: oklch(0.55 0.20 25);
--color-error-bg: oklch(0.97 0.04 25);
--color-error-fg: #ffffff;
--color-warning: oklch(0.65 0.18 75);
--color-warning-bg: oklch(0.97 0.04 75);
--color-warning-fg: oklch(0.20 0.05 75);
}

.dark {
--color-bg: oklch(0.15 0.008 265);
--color-bg-subtle: oklch(0.18 0.008 265);
--color-surface: oklch(0.21 0.008 265);
--color-surface-raised: oklch(0.24 0.008 265);
--color-border: oklch(0.28 0.008 265);
--color-border-strong: oklch(0.35 0.008 265);
--color-text: oklch(0.95 0.008 265);
--color-text-muted: oklch(0.65 0.008 265);
--color-text-subtle: oklch(0.50 0.008 265);
--color-primary: var(--color-brand-400);
--color-primary-hover: var(--color-brand-300);
--color-primary-fg: oklch(0.15 0.005 291);
}
rules:
- Components must only reference semantic tokens, never primitive ramp values directly
- Dark mode requires bg-color elevation (lighter bg per level) since shadows are invisible
- Status color L should be ~0.55 for 4.5:1 contrast with white foreground
- Dark mode primary shifts from brand-500 (light) to brand-400 (dark) for perceptual brightness parity
gotchas:
- "Status colors at L=0.63 (typical green/red) fail 4.5:1 AA with white. Always run contrast check after defining status colors."
- "Do not use hex values for semantic tokens - use oklch primitives so the color stays in the defined color space."
41 changes: 41 additions & 0 deletions corpus/frontend/design-tokens/step-3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
step: 3
title: Bridge to Tailwind v4 utilities with @theme inline
description: >-
Use @theme inline to expose runtime-swappable CSS custom properties as
Tailwind utility classes.
code: |
/* @theme inline READS from CSS vars at runtime */
/* This means dark mode changes propagate to Tailwind utilities automatically */
@theme inline {
/* Map semantic tokens to Tailwind utility names */
--color-background: var(--color-bg);
--color-foreground: var(--color-text);
--color-muted: var(--color-text-muted);
--color-border: var(--color-border);
--color-surface: var(--color-surface);

--color-primary: var(--color-primary);
--color-primary-foreground: var(--color-primary-fg);
--color-secondary: var(--color-secondary);
--color-secondary-foreground: var(--color-secondary-fg);

--color-success: var(--color-success);
--color-success-foreground: var(--color-success-fg);
--color-error: var(--color-error);
--color-error-foreground: var(--color-error-fg);
--color-warning: var(--color-warning);
--color-warning-foreground: var(--color-warning-fg);
}

/* Now these work as Tailwind utilities: */
/* bg-background, text-foreground, bg-primary, text-primary-foreground */
/* border-border, text-muted, bg-surface, bg-success, text-error... */
rules:
- "@theme inline is read at runtime - it reflects CSS custom property values dynamically"
- Plain @theme is static - values are baked in at build time
- Use @theme inline ONLY for semantic tokens; use plain @theme for primitive ramps
- "Name mapping: --color-X in @theme inline → bg-X, text-X, border-X Tailwind classes"
gotchas:
- "If you put runtime-swappable vars in plain @theme (not inline), dark mode will NOT work - the values won't update when .dark class is toggled."
- "@theme inline does not accept hardcoded values - it should only map CSS var references."
- "@theme generates Tailwind utility classes but does NOT emit CSS custom properties on :root. If :root uses var(--color-brand-400) and --color-brand-400 is only defined in @theme, the var() resolves to undefined at runtime - making all text and backgrounds white. Correct architecture: raw OKLCH values on :root and .dark (runtime CSS vars), @theme inline bridges them to utilities, @theme separately generates primitive ramp utilities."
31 changes: 31 additions & 0 deletions corpus/frontend/design-tokens/step-4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
step: 4
title: Define spacing system
description: Set the 4px base multiplier and create named semantic spacing tokens.
code: |
@theme {
/* 4px base - sets the multiplier for p-1, p-2, p-4, etc. */
--spacing: 0.25rem;
}

/* Named semantic tokens in :root */
:root {
--spacing-section-y: 6rem; /* 96px */
--spacing-section-x: 1.5rem; /* 24px mobile, override at md */
--spacing-card: 1.75rem; /* 28px */
--spacing-card-sm: 1.25rem; /* 20px */
--spacing-grid-cards: 1.5rem; /* 24px */
--spacing-inline: 0.5rem; /* 8px */
--spacing-form-gap: 1.25rem; /* 20px */
}

@media (min-width: 768px) {
:root {
--spacing-section-x: 3rem; /* 48px desktop */
}
}
rules:
- "--spacing is the global multiplier - changing it scales ALL numeric spacing utilities"
- "Named tokens auto-generate Tailwind utilities: p-card, py-section-y, gap-grid-cards"
- "Always follow 4px grid: 0.25rem, 0.5rem, 0.75rem, 1rem, 1.25rem, 1.5rem, 1.75rem..."
gotchas:
- "--spacing is NOT an individual token for a specific spacing value. It is the base MULTIPLIER that affects p-1, p-2, p-4, etc. Overriding it changes every numeric spacing utility in the project."
34 changes: 34 additions & 0 deletions corpus/frontend/design-tokens/step-5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
step: 5
title: Set up typography scale
description: >-
Define fluid heading sizes with clamp(), fixed body sizes, and line-height
and tracking tokens.
code: |
:root {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;

--text-display: clamp(3rem, 5vw + 1rem, 4.5rem);
--text-h1: clamp(2.25rem, 3.5vw + 0.5rem, 3rem);
--text-h2: clamp(1.75rem, 2.5vw + 0.25rem, 2.25rem);
--text-h3: clamp(1.375rem, 1.5vw + 0.25rem, 1.5rem);
--text-body: 1rem;
--text-body-sm: 0.875rem;
--text-caption: 0.75rem;
--text-overline: 0.6875rem;

--leading-display: 1.05;
--leading-heading: 1.2;
--leading-body: 1.7;

--tracking-tight: -0.03em;
--tracking-heading: -0.02em;
--tracking-normal: 0em;
--tracking-wide: 0.10em;
}
rules:
- Body text (16px) is NEVER fluid - use fixed rem values
- Headings MUST have tight line-height (1.05-1.2), not body line-height (1.6+)
- Negative tracking on body text is a readability error
gotchas:
- Applying clamp() to body text causes font-size to change on window resize, causing reflow and jarring user experience.
33 changes: 33 additions & 0 deletions corpus/frontend/design-tokens/step-6.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
step: 6
title: Define shadows and elevation
description: >-
Build the 5-level elevation shadow system using warm oklch-tinted shadows.
code: |
:root {
--shadow-xs: 0 1px 2px 0 oklch(0.30 0.02 60 / 0.08);
--shadow-sm:
0 1px 3px 0 oklch(0.30 0.02 60 / 0.10),
0 1px 2px -1px oklch(0.30 0.02 60 / 0.06);
--shadow-md:
0 4px 6px -1px oklch(0.30 0.02 60 / 0.10),
0 2px 4px -2px oklch(0.30 0.02 60 / 0.06);
--shadow-lg:
0 10px 15px -3px oklch(0.30 0.02 60 / 0.10),
0 4px 6px -4px oklch(0.30 0.02 60 / 0.05);
--shadow-xl:
0 20px 25px -5px oklch(0.30 0.02 60 / 0.10),
0 8px 10px -6px oklch(0.30 0.02 60 / 0.04);
}

.dark {
--shadow-xs: none; --shadow-sm: none;
--shadow-md: none; --shadow-lg: none; --shadow-xl: none;
--color-surface-1: oklch(0.22 0.008 265);
--color-surface-2: oklch(0.26 0.008 265);
--color-surface-3: oklch(0.30 0.008 265);
}
rules:
- Use oklch-tinted shadows, never rgba(0,0,0)
- Dark mode disables all shadows, uses bg-color elevation instead
gotchas:
- rgba(0,0,0) shadows look cold on warm backgrounds. The warm hue tint (H=60) makes shadows feel natural.
32 changes: 32 additions & 0 deletions corpus/frontend/design-tokens/step-7.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
step: 7
title: Define motion and z-index tokens
description: Add duration, easing, and z-index scale with prefers-reduced-motion.
code: |
@theme {
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);

--z-dropdown: 1000;
--z-sticky: 1010;
--z-modal: 1050;
--z-tooltip: 1070;
--z-toast: 1080;
}

@layer base {
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
}
rules:
- prefers-reduced-motion MUST be in @layer base with !important
- Never use arbitrary z-index values
gotchas:
- Forgetting prefers-reduced-motion is a WCAG 2.3.3 violation.
27 changes: 27 additions & 0 deletions corpus/frontend/design-tokens/step-8.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
step: 8
title: Run contrast audit and verify token usage
description: >-
After building the full token system, run a contrast audit on all color
token pairs.
code: |
/* Run contrast checks on these pairs: */
/* --color-text on --color-bg → target 7:1 (AAA) */
/* --color-text-muted on --color-bg → target 4.5:1 (AA) */
/* --color-primary-fg on --color-primary → target 4.5:1 (AA) */
/* --color-success-fg on --color-success → target 4.5:1 (AA) */
/* --color-error-fg on --color-error → target 4.5:1 (AA) */
/* --color-border on --color-bg → target 1.1:1+ (perceptible) */

/* Tools: */
/* - https://oklch.com - build and preview oklch colors */
/* - https://www.myndex.com/APCA/ - APCA contrast checker */
/* - Storybook a11y addon - automated component-level checks */

/* Check for stale hue references: */
/* grep -r "violet-\|purple-\|blue-" src/ (old hardcoded Tailwind hue names) */
rules:
- Run contrast audit AFTER finalizing the palette - before building components
- Status colors often need L adjusted to ~0.55 for AA compliance with white foreground
- Grep for stale hue-name references after renaming ramps
gotchas:
- "WCAG contrast is calculated on final rendered colors. If CSS vars are not resolving correctly in dark mode, contrast tools won't catch it - test with a browser extension on the live page."
Loading
Loading