From 5be2c48afe53b654bae4313d9bfc6d14f760756c Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 28 May 2026 11:26:56 -0700 Subject: [PATCH] v0.21.0 foundation: design-token module (#31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establish src/styles/tokens.css — the single source of truth for color, type, spacing, radius, elevation, and motion. Imported first in main.jsx. Sweep subagents map literal values to these tokens; no appearance change is intended beyond consolidating near-duplicate grays (operator reviews before deploy). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/main.jsx | 1 + frontend/src/styles/tokens.css | 162 +++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 frontend/src/styles/tokens.css diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index f4fa3e5..07c4282 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,6 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import App from './App.jsx' +import './styles/tokens.css' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css new file mode 100644 index 0000000..5d4c7dd --- /dev/null +++ b/frontend/src/styles/tokens.css @@ -0,0 +1,162 @@ +/* tokens.css — the design-token foundation for rfc-app's UI. + * + * Roadmap item #31 (comprehensive UX polish). Before this file the app + * had ~98 distinct hardcoded hex colors, font sizes scattered across 16 + * values with no scale, and radii across 13 values — classic prototype + * sprawl. This module establishes ONE coherent system; the App.css sweep + * (and component-scoped CSS) reference these custom properties instead of + * literal values, so "what color/size/space is this" has a single answer. + * + * Imported FIRST in main.jsx so :root is defined before any other sheet. + * Custom properties are not cascade-order-sensitive at use time, but + * importing first keeps the dependency obvious. + * + * Conventions for anyone sweeping values to these tokens: + * - Map each literal to the NEAREST semantic token, then fall back to a + * primitive ramp step. Consolidating near-duplicate grays is the point. + * - Never invent a new literal in a component; add a token here instead. + * - Spacing/radii/type use the scales below — no off-scale px values. + */ + +:root { + /* ===== Color primitives — neutral ramp ===== */ + --c-white: #ffffff; + --c-gray-50: #fafafa; + --c-gray-100: #f3f4f6; + --c-gray-150: #f0f0ee; /* the app's warm canvas tint */ + --c-gray-200: #e5e7eb; + --c-gray-300: #d1d5db; + --c-gray-400: #9ca3af; + --c-gray-500: #6b7280; + --c-gray-600: #4b5563; + --c-gray-700: #374151; + --c-gray-800: #1f2937; + --c-gray-900: #111111; + --c-ink: #1a1a1a; /* near-black used for the header + body text */ + + /* ===== Color primitives — accent (indigo/violet) ===== */ + --c-accent: #5b5bd6; + --c-accent-strong: #4338ca; + --c-violet: #7c3aed; + + /* ===== Color primitives — status ===== */ + --c-success-fg: #166534; + --c-success-bg: #dcfce7; + --c-danger-fg: #991b1b; + --c-danger-fg-strong: #b91c1c; + --c-danger-bg: #fef2f2; + --c-danger-border: #fecaca; + --c-warning-fg: #92400e; + --c-warning-accent: #b45309; + --c-warning-bg: #fef3c7; + --c-warning-bg-soft:#fffbeb; + + /* ===== Semantic colors ===== */ + --color-bg: var(--c-gray-150); + --color-surface: var(--c-white); + --color-surface-sunken: var(--c-gray-50); + --color-surface-muted: var(--c-gray-100); + --color-header-bg: var(--c-ink); + + --color-text: var(--c-ink); + --color-text-strong: var(--c-gray-900); + --color-text-muted: var(--c-gray-500); + --color-text-subtle: var(--c-gray-400); + --color-text-inverse: var(--c-white); + + --color-border: var(--c-gray-200); + --color-border-strong: var(--c-gray-300); + + --color-link: var(--c-accent); + --color-accent: var(--c-accent); + --color-accent-strong: var(--c-accent-strong); + --color-accent-contrast: var(--c-white); + + --color-success-fg: var(--c-success-fg); + --color-success-bg: var(--c-success-bg); + --color-danger-fg: var(--c-danger-fg); + --color-danger-bg: var(--c-danger-bg); + --color-warning-fg: var(--c-warning-fg); + --color-warning-bg: var(--c-warning-bg); + + /* On the dark header, translucent white is the established pattern. */ + --color-on-dark-soft: rgba(255, 255, 255, 0.15); + --color-on-dark-hover: rgba(255, 255, 255, 0.25); + --color-on-dark-muted: #dddddd; + + --color-focus-ring: rgba(91, 91, 214, 0.45); + + /* ===== Type ===== */ + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; + + --text-2xs: 10px; + --text-xs: 11px; + --text-sm: 12px; + --text-base: 13px; /* the app's dominant body size */ + --text-md: 14px; + --text-lg: 16px; + --text-xl: 18px; + --text-2xl: 22px; + --text-3xl: 28px; + + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.65; + + --weight-normal: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + + /* ===== Spacing scale (4-based, with the 2/6/10 half-steps the app + * already leans on heavily) ===== */ + --space-0: 0; + --space-1: 2px; + --space-2: 4px; + --space-3: 6px; + --space-4: 8px; + --space-5: 10px; + --space-6: 12px; + --space-7: 16px; + --space-8: 20px; + --space-9: 24px; + --space-10: 32px; + --space-11: 48px; + --space-12: 64px; + + /* ===== Radius ===== */ + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-pill: 999px; + + /* ===== Elevation ===== */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + + /* ===== Motion ===== */ + --motion-fast: 120ms; + --motion-base: 150ms; + --motion-slow: 200ms; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + + /* ===== Layout ===== */ + --header-height: 48px; +} + +/* Honor reduced-motion globally — any transition/animation that reads + * these duration tokens collapses to instant. */ +@media (prefers-reduced-motion: reduce) { + :root { + --motion-fast: 0ms; + --motion-base: 0ms; + --motion-slow: 0ms; + } +}