diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..50b113f --- /dev/null +++ b/src/styles/tokens.css @@ -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; + } +} \ No newline at end of file diff --git a/tests/verify-tokens.mjs b/tests/verify-tokens.mjs new file mode 100644 index 0000000..b3eae5f --- /dev/null +++ b/tests/verify-tokens.mjs @@ -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!'); +} \ No newline at end of file