feat: add design tokens as CSS custom properties
Create src/styles/tokens.css with complete design token system including: - Surface, text, accent, code, border, focus colors - State colors (success, warning, error, info) - Shadow definitions - Typography families with full fallback stacks - Typography scale (xs through 3xl) and line heights - Spacing scale (0 through 12) - Border radius tokens - Layout constraints (prose, code, container widths) - Motion tokens with reduced-motion media query All values sourced from .design/specs/design-tokens-2026-05-26.md. Includes verification test with 56 assertions across all categories.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
:root {
|
||||
/* Surfaces */
|
||||
--surface-base: #F5F0E8;
|
||||
--surface-card: #FEFCF7;
|
||||
--surface-code: #EEEAE2;
|
||||
--surface-inset: #E8E3DA;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #2B2421;
|
||||
--text-secondary: #6B5F55;
|
||||
--text-tertiary: #948880;
|
||||
|
||||
/* Accents */
|
||||
--accent-primary: #A0522D;
|
||||
--accent-primary-hover: #8B4726;
|
||||
--accent-secondary: #7D6C2F;
|
||||
--accent-secondary-hover: #6B5C28;
|
||||
|
||||
/* Code */
|
||||
--code-text: #3D342E;
|
||||
|
||||
/* Borders */
|
||||
--border-subtle: #DDD7CD;
|
||||
--border-default: #C2BAA9;
|
||||
--border-strong: #8E857A;
|
||||
|
||||
/* Focus */
|
||||
--focus-ring: #A0522D;
|
||||
|
||||
/* State: Success */
|
||||
--success-bg: #E8EDDF;
|
||||
--success-text: #4A5E3A;
|
||||
--success-border: #B5C4A0;
|
||||
|
||||
/* State: Warning */
|
||||
--warning-bg: #F2E8D0;
|
||||
--warning-text: #7A5C1F;
|
||||
--warning-border: #D4BC7C;
|
||||
|
||||
/* State: Error */
|
||||
--error-bg: #F2DED6;
|
||||
--error-text: #8B3A2A;
|
||||
--error-border: #CFA090;
|
||||
|
||||
/* State: Info */
|
||||
--info-bg: #E5E2DA;
|
||||
--info-text: #4A4540;
|
||||
--info-border: #B8B2A8;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 3px rgba(43, 36, 33, 0.04);
|
||||
--shadow-md: 0 2px 8px rgba(43, 36, 33, 0.06);
|
||||
|
||||
/* Typography families */
|
||||
--font-prose: 'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif;
|
||||
--font-structure: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-evidence: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace;
|
||||
|
||||
/* Typography scale */
|
||||
--type-xs: 0.75rem;
|
||||
--type-sm: 0.875rem;
|
||||
--type-base: 1.125rem;
|
||||
--type-lg: 1.375rem;
|
||||
--type-xl: 1.75rem;
|
||||
--type-2xl: 2.25rem;
|
||||
--type-3xl: 2.75rem;
|
||||
|
||||
/* Line heights */
|
||||
--leading-tight: 1.15;
|
||||
--leading-snug: 1.25;
|
||||
--leading-heading: 1.35;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.6;
|
||||
--leading-prose: 1.7;
|
||||
|
||||
/* Spacing */
|
||||
--space-0: 0;
|
||||
--space-px: 1px;
|
||||
--space-0-5: 0.25rem;
|
||||
--space-1: 0.5rem;
|
||||
--space-1-5: 0.75rem;
|
||||
--space-2: 1rem;
|
||||
--space-2-5: 1.25rem;
|
||||
--space-3: 1.5rem;
|
||||
--space-4: 2rem;
|
||||
--space-5: 2.5rem;
|
||||
--space-6: 3rem;
|
||||
--space-8: 4rem;
|
||||
--space-10: 5rem;
|
||||
--space-12: 6rem;
|
||||
|
||||
/* Border radius */
|
||||
--rounded-none: 0;
|
||||
--rounded-sm: 2px;
|
||||
--rounded-md: 4px;
|
||||
--rounded-lg: 6px;
|
||||
--rounded-xl: 8px;
|
||||
--rounded-full: 9999px;
|
||||
|
||||
/* Layout */
|
||||
--width-prose: 45rem;
|
||||
--width-code: 52.5rem;
|
||||
--width-container: 68.75rem;
|
||||
--gutter: 1.5rem;
|
||||
|
||||
/* Motion */
|
||||
--duration-fast: 100ms;
|
||||
--duration-normal: 150ms;
|
||||
--duration-slow: 250ms;
|
||||
--ease-default: cubic-bezier(0.25, 0.1, 0.25, 1.0);
|
||||
--ease-in-out: cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--duration-fast: 0ms;
|
||||
--duration-normal: 0ms;
|
||||
--duration-slow: 0ms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Verification test for Design Tokens CSS (Task 2)
|
||||
*
|
||||
* Checks that:
|
||||
* 1. src/styles/tokens.css exists
|
||||
* 2. Contains :root block with all token categories
|
||||
* 3. Has ~60-70 CSS custom properties
|
||||
* 4. Includes reduced motion media query
|
||||
* 5. All expected token categories are present
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, '..');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (condition) {
|
||||
console.log(` ✓ ${message}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` ✗ ${message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function readText(path) {
|
||||
return readFileSync(path, 'utf8');
|
||||
}
|
||||
|
||||
const tokensPath = join(root, 'src/styles/tokens.css');
|
||||
|
||||
// === 1. File exists ===
|
||||
console.log('\n1. tokens.css exists');
|
||||
assert(existsSync(tokensPath), 'src/styles/tokens.css exists');
|
||||
|
||||
if (!existsSync(tokensPath)) {
|
||||
console.log('\nFile not found, skipping remaining tests.');
|
||||
console.log(`\n${'='.repeat(40)}`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const css = readText(tokensPath);
|
||||
|
||||
// === 2. Contains :root block ===
|
||||
console.log('\n2. :root block');
|
||||
assert(css.includes(':root {'), 'contains :root { block');
|
||||
|
||||
// === 3. Token count ~60-70 ===
|
||||
console.log('\n3. Custom property count');
|
||||
const propertyMatches = css.match(/--[\w-]+\s*:/g);
|
||||
const totalProps = propertyMatches ? propertyMatches.length : 0;
|
||||
// Inside :root only (exclude media query overrides)
|
||||
const rootBlock = css.match(/:root\s*\{([^}]*)\}/);
|
||||
const rootContent = rootBlock ? rootBlock[1] : '';
|
||||
const rootProps = rootContent.match(/--[\w-]+\s*:/g);
|
||||
const rootPropCount = rootProps ? rootProps.length : 0;
|
||||
assert(rootPropCount >= 55 && rootPropCount <= 75, `root has ~60-70 custom properties (found ${rootPropCount})`);
|
||||
|
||||
// === 4. Surface tokens ===
|
||||
console.log('\n4. Surface tokens');
|
||||
assert(css.includes('--surface-base: #F5F0E8'), '--surface-base: #F5F0E8');
|
||||
assert(css.includes('--surface-card: #FEFCF7'), '--surface-card: #FEFCF7');
|
||||
assert(css.includes('--surface-code: #EEEAE2'), '--surface-code: #EEEAE2');
|
||||
assert(css.includes('--surface-inset: #E8E3DA'), '--surface-inset: #E8E3DA');
|
||||
|
||||
// === 5. Text tokens ===
|
||||
console.log('\n5. Text tokens');
|
||||
assert(css.includes('--text-primary: #2B2421'), '--text-primary');
|
||||
assert(css.includes('--text-secondary: #6B5F55'), '--text-secondary');
|
||||
assert(css.includes('--text-tertiary: #948880'), '--text-tertiary');
|
||||
|
||||
// === 6. Accent tokens ===
|
||||
console.log('\n6. Accent tokens');
|
||||
assert(css.includes('--accent-primary: #A0522D'), '--accent-primary');
|
||||
assert(css.includes('--accent-primary-hover: #8B4726'), '--accent-primary-hover');
|
||||
assert(css.includes('--accent-secondary: #7D6C2F'), '--accent-secondary');
|
||||
assert(css.includes('--accent-secondary-hover: #6B5C28'), '--accent-secondary-hover');
|
||||
|
||||
// === 7. Code token ===
|
||||
console.log('\n7. Code token');
|
||||
assert(css.includes('--code-text: #3D342E'), '--code-text');
|
||||
|
||||
// === 8. Border tokens ===
|
||||
console.log('\n8. Border tokens');
|
||||
assert(css.includes('--border-subtle: #DDD7CD'), '--border-subtle');
|
||||
assert(css.includes('--border-default: #C2BAA9'), '--border-default');
|
||||
assert(css.includes('--border-strong: #8E857A'), '--border-strong');
|
||||
|
||||
// === 9. Focus token ===
|
||||
console.log('\n9. Focus token');
|
||||
assert(css.includes('--focus-ring: #A0522D'), '--focus-ring');
|
||||
|
||||
// === 10. State tokens ===
|
||||
console.log('\n10. State tokens');
|
||||
assert(css.includes('--success-bg:'), 'success-bg');
|
||||
assert(css.includes('--success-text:'), 'success-text');
|
||||
assert(css.includes('--success-border:'), 'success-border');
|
||||
assert(css.includes('--warning-bg:'), 'warning-bg');
|
||||
assert(css.includes('--warning-text:'), 'warning-text');
|
||||
assert(css.includes('--warning-border:'), 'warning-border');
|
||||
assert(css.includes('--error-bg:'), 'error-bg');
|
||||
assert(css.includes('--error-text:'), 'error-text');
|
||||
assert(css.includes('--error-border:'), 'error-border');
|
||||
assert(css.includes('--info-bg:'), 'info-bg');
|
||||
assert(css.includes('--info-text:'), 'info-text');
|
||||
assert(css.includes('--info-border:'), 'info-border');
|
||||
|
||||
// === 11. Shadow tokens ===
|
||||
console.log('\n11. Shadow tokens');
|
||||
assert(css.includes('--shadow-sm:'), '--shadow-sm');
|
||||
assert(css.includes('--shadow-md:'), '--shadow-md');
|
||||
|
||||
// === 12. Typography family tokens ===
|
||||
console.log('\n12. Typography family tokens');
|
||||
assert(css.includes('--font-prose:') && css.includes('Source Serif 4'), '--font-prose with Source Serif 4');
|
||||
assert(css.includes('--font-structure:') && css.includes('Inter'), '--font-structure with Inter');
|
||||
assert(css.includes('--font-evidence:') && css.includes('JetBrains Mono'), '--font-evidence with JetBrains Mono');
|
||||
|
||||
// === 13. Spacing tokens ===
|
||||
console.log('\n13. Spacing tokens');
|
||||
assert(css.includes('--space-0:'), '--space-0');
|
||||
assert(css.includes('--space-1:'), '--space-1');
|
||||
assert(css.includes('--space-4:'), '--space-4');
|
||||
assert(css.includes('--space-12:'), '--space-12');
|
||||
|
||||
// === 14. Border radius tokens ===
|
||||
console.log('\n14. Border radius tokens');
|
||||
assert(css.includes('--rounded-none:'), '--rounded-none');
|
||||
assert(css.includes('--rounded-sm:'), '--rounded-sm');
|
||||
assert(css.includes('--rounded-full:'), '--rounded-full');
|
||||
|
||||
// === 15. Layout tokens ===
|
||||
console.log('\n15. Layout tokens');
|
||||
assert(css.includes('--width-prose: 45rem'), '--width-prose');
|
||||
assert(css.includes('--width-code: 52.5rem'), '--width-code');
|
||||
assert(css.includes('--width-container: 68.75rem'), '--width-container');
|
||||
assert(css.includes('--gutter: 1.5rem'), '--gutter');
|
||||
|
||||
// === 16. Motion tokens ===
|
||||
console.log('\n16. Motion tokens');
|
||||
assert(css.includes('--duration-fast:'), '--duration-fast');
|
||||
assert(css.includes('--duration-normal:'), '--duration-normal');
|
||||
assert(css.includes('--duration-slow:'), '--duration-slow');
|
||||
assert(css.includes('--ease-default:'), '--ease-default');
|
||||
assert(css.includes('--ease-in-out:'), '--ease-in-out');
|
||||
|
||||
// === 17. Reduced motion media query ===
|
||||
console.log('\n17. Reduced motion media query');
|
||||
assert(css.includes('prefers-reduced-motion: reduce'), 'has prefers-reduced-motion media query');
|
||||
assert(css.includes('--duration-fast: 0ms'), 'duration-fast zeroed in reduced motion');
|
||||
assert(css.includes('--duration-normal: 0ms'), 'duration-normal zeroed in reduced motion');
|
||||
assert(css.includes('--duration-slow: 0ms'), 'duration-slow zeroed in reduced motion');
|
||||
|
||||
// === Summary ===
|
||||
console.log(`\n${'='.repeat(40)}`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed}`);
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('All token verification tests passed!');
|
||||
}
|
||||
Reference in New Issue
Block a user