build-tokens.js
1 #!/usr/bin/env node 2 /** 3 * ACDC Design System - Token Build Pipeline 4 * 5 * Single source of truth: JSON tokens ā CSS + TypeScript + Tailwind 6 * 7 * Outputs: 8 * - dist/tokens.css (CSS custom properties for all themes) 9 * - dist/tokens.ts (TypeScript constants with types) 10 * - tailwind.preset.js (Tailwind configuration) 11 */ 12 13 import fs from 'fs'; 14 import path from 'path'; 15 import { fileURLToPath } from 'url'; 16 17 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 18 const ROOT = path.join(__dirname, '..'); 19 const TOKENS_DIR = path.join(ROOT, 'tokens'); 20 const THEMES_DIR = path.join(ROOT, 'themes'); 21 const DIST_DIR = path.join(ROOT, 'dist'); 22 const STYLES_DIR = path.join(ROOT, 'src', 'styles'); 23 24 // Ensure output directories exist 25 fs.mkdirSync(DIST_DIR, { recursive: true }); 26 fs.mkdirSync(STYLES_DIR, { recursive: true }); 27 28 console.log('šØ Building ACDC Design Tokens...\n'); 29 30 // ============================================================================= 31 // Load Source Files 32 // ============================================================================= 33 34 const colors = JSON.parse(fs.readFileSync(path.join(TOKENS_DIR, 'colors.json'), 'utf-8')); 35 const typography = JSON.parse(fs.readFileSync(path.join(TOKENS_DIR, 'typography.json'), 'utf-8')); 36 const spacing = JSON.parse(fs.readFileSync(path.join(TOKENS_DIR, 'spacing.json'), 'utf-8')); 37 38 // Load themes 39 const lightTheme = JSON.parse(fs.readFileSync(path.join(THEMES_DIR, 'light.json'), 'utf-8')); 40 const darkTheme = JSON.parse(fs.readFileSync(path.join(THEMES_DIR, 'dark.json'), 'utf-8')); 41 42 let highContrastTheme, testnetTheme; 43 try { 44 highContrastTheme = JSON.parse(fs.readFileSync(path.join(THEMES_DIR, 'high-contrast.json'), 'utf-8')); 45 } catch { highContrastTheme = null; } 46 try { 47 testnetTheme = JSON.parse(fs.readFileSync(path.join(THEMES_DIR, 'testnet.json'), 'utf-8')); 48 } catch { testnetTheme = null; } 49 50 // ============================================================================= 51 // Extended Color Scales (for Tailwind) 52 // ============================================================================= 53 54 // Generate full color scale from brand colors 55 const alphaScale = { 56 50: '#EBF4FF', 57 100: '#D6E8FF', 58 200: '#ADD1FF', 59 300: '#75B3FF', 60 400: '#4A9DFF', 61 500: '#2B87FF', 62 600: '#1A6FE8', 63 700: '#1458CC', 64 800: '#1048A8', 65 900: '#0D3A85', 66 }; 67 68 const deltaScale = { 69 50: '#FFFBEB', 70 100: '#FEF3C7', 71 200: '#FDE68A', 72 300: '#FCD34D', 73 400: '#FBBF24', 74 500: '#F59E0B', 75 600: '#D97706', 76 700: '#B45309', 77 800: '#92400E', 78 900: '#78350F', 79 }; 80 81 // ============================================================================= 82 // Generate CSS Variables 83 // ============================================================================= 84 85 function generateCSS() { 86 const lines = [ 87 '/**', 88 ' * ACDC Design System - CSS Custom Properties', 89 ' * Auto-generated from tokens/*.json - DO NOT EDIT MANUALLY', 90 ' * ', 91 ' * Usage: var(--bg-primary), var(--alpha-500), etc.', 92 ' */', 93 '', 94 ]; 95 96 // Static brand colors (same in all themes) 97 lines.push(':root {'); 98 lines.push(' /* Alpha Chain Color Scale */'); 99 for (const [key, value] of Object.entries(alphaScale)) { 100 lines.push(` --alpha-${key}: ${value};`); 101 } 102 lines.push(''); 103 lines.push(' /* Delta Chain Color Scale */'); 104 for (const [key, value] of Object.entries(deltaScale)) { 105 lines.push(` --delta-${key}: ${value};`); 106 } 107 lines.push(''); 108 109 // Semantic colors 110 lines.push(' /* Semantic Colors */'); 111 lines.push(` --success: ${colors.semantic.success};`); 112 lines.push(` --success-hover: #4ADE80;`); 113 lines.push(` --success-active: #16A34A;`); 114 lines.push(` --warning: ${colors.semantic.warning};`); 115 lines.push(` --warning-hover: #FBBF24;`); 116 lines.push(` --warning-active: #D97706;`); 117 lines.push(` --error: ${colors.semantic.error};`); 118 lines.push(` --error-hover: #F87171;`); 119 lines.push(` --error-active: #DC2626;`); 120 lines.push(` --info: ${colors.semantic.info};`); 121 lines.push(` --info-hover: #60A5FA;`); 122 lines.push(` --info-active: #2563EB;`); 123 lines.push(''); 124 125 // Spacing scale 126 lines.push(' /* Spacing Scale (4px base) */'); 127 for (const [key, value] of Object.entries(spacing.scale)) { 128 const varName = key.replace('.', '-'); 129 lines.push(` --space-${varName}: ${value};`); 130 } 131 lines.push(''); 132 133 // Border radius 134 lines.push(' /* Border Radius */'); 135 lines.push(' --radius-none: 0;'); 136 lines.push(' --radius-xs: 4px;'); 137 lines.push(' --radius-sm: 6px;'); 138 lines.push(' --radius-md: 8px;'); 139 lines.push(' --radius-lg: 12px;'); 140 lines.push(' --radius-xl: 16px;'); 141 lines.push(' --radius-2xl: 24px;'); 142 lines.push(' --radius-full: 9999px;'); 143 lines.push(''); 144 145 // Shadows 146 lines.push(' /* Shadows */'); 147 lines.push(' --shadow-1: 0 1px 2px rgba(0,0,0,0.05);'); 148 lines.push(' --shadow-2: 0 2px 4px rgba(0,0,0,0.05), 0 4px 8px rgba(0,0,0,0.05);'); 149 lines.push(' --shadow-3: 0 4px 8px rgba(0,0,0,0.05), 0 8px 16px rgba(0,0,0,0.08);'); 150 lines.push(' --shadow-4: 0 8px 16px rgba(0,0,0,0.08), 0 16px 32px rgba(0,0,0,0.1);'); 151 lines.push(' --shadow-5: 0 16px 32px rgba(0,0,0,0.1), 0 24px 48px rgba(0,0,0,0.15);'); 152 lines.push(' --glow-alpha: 0 4px 16px rgba(43,135,255,0.2);'); 153 lines.push(' --glow-delta: 0 4px 16px rgba(245,158,11,0.2);'); 154 lines.push('}'); 155 lines.push(''); 156 157 // Light theme (default) 158 lines.push('/* Light Theme (Default) */'); 159 lines.push(':root {'); 160 addThemeVariables(lines, lightTheme); 161 lines.push(''); 162 lines.push(' /* Button defaults (Alpha context) */'); 163 lines.push(' --btn-primary-bg: var(--alpha-500);'); 164 lines.push(' --btn-primary-bg-hover: var(--alpha-600);'); 165 lines.push(' --btn-primary-bg-active: var(--alpha-700);'); 166 lines.push('}'); 167 lines.push(''); 168 169 // Dark theme 170 lines.push('/* Dark Theme */'); 171 lines.push('.dark, [data-theme="dark"] {'); 172 addThemeVariables(lines, darkTheme); 173 lines.push(''); 174 lines.push(' /* Darker shadows for dark mode */'); 175 lines.push(' --shadow-1: 0 1px 2px rgba(0,0,0,0.4);'); 176 lines.push(' --shadow-2: 0 2px 8px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05);'); 177 lines.push(' --shadow-3: 0 4px 16px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);'); 178 lines.push(' --shadow-4: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.08);'); 179 lines.push(' --shadow-5: 0 16px 48px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.1);'); 180 lines.push(' --glow-alpha: 0 0 20px rgba(43,135,255,0.3), 0 0 40px rgba(43,135,255,0.15);'); 181 lines.push(' --glow-delta: 0 0 20px rgba(245,158,11,0.3), 0 0 40px rgba(245,158,11,0.15);'); 182 lines.push('}'); 183 lines.push(''); 184 185 // High contrast theme 186 if (highContrastTheme) { 187 lines.push('/* High Contrast Theme */'); 188 lines.push('[data-theme="high-contrast"] {'); 189 addThemeVariables(lines, highContrastTheme); 190 lines.push('}'); 191 lines.push(''); 192 } 193 194 // Testnet theme 195 if (testnetTheme) { 196 lines.push('/* Testnet Theme */'); 197 lines.push('[data-theme="testnet"] {'); 198 addThemeVariables(lines, testnetTheme); 199 lines.push('}'); 200 lines.push(''); 201 } 202 203 // Chain context overrides 204 lines.push('/* Chain Context - Delta */'); 205 lines.push('[data-chain="delta"] {'); 206 lines.push(' --btn-primary-bg: var(--delta-500);'); 207 lines.push(' --btn-primary-bg-hover: var(--delta-600);'); 208 lines.push(' --btn-primary-bg-active: var(--delta-700);'); 209 lines.push('}'); 210 211 return lines.join('\n'); 212 } 213 214 function addThemeVariables(lines, theme) { 215 if (!theme || !theme.colors) return; 216 217 const c = theme.colors; 218 219 // Background 220 if (c.background) { 221 lines.push(' /* Backgrounds */'); 222 lines.push(` --bg-primary: ${c.background.primary};`); 223 lines.push(` --bg-secondary: ${c.background.secondary};`); 224 lines.push(` --bg-tertiary: ${c.background.tertiary || c.background.secondary};`); 225 lines.push(` --bg-elevated: ${c.card?.background || c.background.primary};`); 226 } 227 228 // Text 229 if (c.foreground) { 230 lines.push(' /* Text */'); 231 lines.push(` --text-primary: ${c.foreground.primary};`); 232 lines.push(` --text-secondary: ${c.foreground.secondary};`); 233 lines.push(` --text-tertiary: ${c.foreground.muted || c.foreground.secondary};`); 234 lines.push(` --text-muted: ${c.foreground.muted};`); 235 } 236 237 // Borders 238 if (c.border) { 239 lines.push(' /* Borders */'); 240 lines.push(` --border-subtle: ${c.border.default};`); 241 lines.push(` --border-default: ${c.border.strong || c.border.default};`); 242 lines.push(` --border-strong: ${c.border.strong};`); 243 } 244 245 // Input focus 246 if (c.input?.focus) { 247 lines.push(` --input-border-focus: ${c.input.focus};`); 248 } 249 } 250 251 // ============================================================================= 252 // Generate TypeScript Constants 253 // ============================================================================= 254 255 function generateTypeScript() { 256 return `/** 257 * ACDC Design System - TypeScript Token Constants 258 * Auto-generated from tokens/*.json - DO NOT EDIT MANUALLY 259 */ 260 261 export const colors = { 262 alpha: ${JSON.stringify(alphaScale, null, 4)}, 263 delta: ${JSON.stringify(deltaScale, null, 4)}, 264 semantic: ${JSON.stringify(colors.semantic, null, 4)}, 265 neutral: ${JSON.stringify(colors.neutral, null, 4)}, 266 } as const; 267 268 export const spacing = ${JSON.stringify(spacing.scale, null, 2)} as const; 269 270 export const typography = { 271 families: ${JSON.stringify(typography.families, null, 4)}, 272 scale: ${JSON.stringify(typography.scale, null, 4)}, 273 weights: ${JSON.stringify(typography.weights, null, 4)}, 274 } as const; 275 276 export type Theme = 'light' | 'dark' | 'high-contrast' | 'testnet' | 'system'; 277 export type Chain = 'alpha' | 'delta'; 278 export type ColorScale = keyof typeof colors.alpha; 279 `; 280 } 281 282 // ============================================================================= 283 // Generate Tailwind Preset 284 // ============================================================================= 285 286 function generateTailwindPreset() { 287 return `/** 288 * ACDC Design System - Tailwind Preset 289 * Auto-generated from tokens/*.json - DO NOT EDIT MANUALLY 290 * 291 * Usage: 292 * import acdcPreset from '@acdc/design/tailwind' 293 * export default { presets: [acdcPreset], ... } 294 */ 295 296 /** @type {import('tailwindcss').Config} */ 297 export default { 298 content: [], 299 darkMode: 'class', 300 301 theme: { 302 extend: { 303 // === COLORS === 304 colors: { 305 // Alpha Chain (Blue) 306 alpha: ${JSON.stringify(alphaScale, null, 8).replace(/\n/g, '\n ')}, 307 // Delta Chain (Amber) 308 delta: ${JSON.stringify(deltaScale, null, 8).replace(/\n/g, '\n ')}, 309 // Semantic 310 success: { 311 DEFAULT: '${colors.semantic.success}', 312 hover: '#4ADE80', 313 active: '#16A34A', 314 }, 315 warning: { 316 DEFAULT: '${colors.semantic.warning}', 317 hover: '#FBBF24', 318 active: '#D97706', 319 }, 320 error: { 321 DEFAULT: '${colors.semantic.error}', 322 hover: '#F87171', 323 active: '#DC2626', 324 }, 325 info: { 326 DEFAULT: '${colors.semantic.info}', 327 hover: '#60A5FA', 328 active: '#2563EB', 329 }, 330 // Theme-aware (via CSS variables) 331 bg: { 332 primary: 'var(--bg-primary)', 333 secondary: 'var(--bg-secondary)', 334 tertiary: 'var(--bg-tertiary)', 335 elevated: 'var(--bg-elevated)', 336 }, 337 text: { 338 primary: 'var(--text-primary)', 339 secondary: 'var(--text-secondary)', 340 tertiary: 'var(--text-tertiary)', 341 muted: 'var(--text-muted)', 342 }, 343 border: { 344 subtle: 'var(--border-subtle)', 345 DEFAULT: 'var(--border-default)', 346 strong: 'var(--border-strong)', 347 }, 348 }, 349 350 // === TYPOGRAPHY === 351 fontFamily: { 352 sans: ['${typography.families.sans}'], 353 mono: ['${typography.families.mono}'], 354 display: ['${typography.families.display || typography.families.sans}'], 355 }, 356 fontSize: { 357 xs: ['0.75rem', { lineHeight: '1rem' }], 358 sm: ['0.875rem', { lineHeight: '1.25rem' }], 359 base: ['1rem', { lineHeight: '1.5rem' }], 360 lg: ['1.125rem', { lineHeight: '1.75rem' }], 361 xl: ['1.25rem', { lineHeight: '1.75rem' }], 362 '2xl': ['1.5rem', { lineHeight: '2rem' }], 363 '3xl': ['1.875rem', { lineHeight: '2.25rem' }], 364 '4xl': ['2.25rem', { lineHeight: '2.5rem' }], 365 '5xl': ['3rem', { lineHeight: '1' }], 366 // Semantic sizes 367 display: ['56px', { lineHeight: '1.1', letterSpacing: '-0.02em', fontWeight: '700' }], 368 h1: ['40px', { lineHeight: '1.2', letterSpacing: '-0.02em', fontWeight: '700' }], 369 h2: ['32px', { lineHeight: '1.25', letterSpacing: '-0.01em', fontWeight: '700' }], 370 h3: ['24px', { lineHeight: '1.3', fontWeight: '600' }], 371 h4: ['20px', { lineHeight: '1.4', fontWeight: '600' }], 372 'body-lg': ['18px', { lineHeight: '1.6' }], 373 body: ['16px', { lineHeight: '1.6' }], 374 'body-sm': ['14px', { lineHeight: '1.5' }], 375 caption: ['12px', { lineHeight: '1.4', letterSpacing: '0.02em', fontWeight: '500' }], 376 }, 377 378 // === SPACING === 379 spacing: ${JSON.stringify(spacing.scale, null, 8).replace(/\n/g, '\n ')}, 380 381 // === BORDER RADIUS === 382 borderRadius: { 383 none: '0', 384 xs: '4px', 385 sm: '6px', 386 md: '8px', 387 lg: '12px', 388 xl: '16px', 389 '2xl': '24px', 390 full: '9999px', 391 }, 392 393 // === SHADOWS === 394 boxShadow: { 395 1: 'var(--shadow-1)', 396 2: 'var(--shadow-2)', 397 3: 'var(--shadow-3)', 398 4: 'var(--shadow-4)', 399 5: 'var(--shadow-5)', 400 'glow-alpha': 'var(--glow-alpha)', 401 'glow-delta': 'var(--glow-delta)', 402 sm: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08)', 403 md: '0 4px 12px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.08)', 404 lg: '0 12px 24px rgba(0,0,0,0.18), 0 4px 8px rgba(0,0,0,0.08)', 405 xl: '0 20px 40px rgba(0,0,0,0.2), 0 8px 16px rgba(0,0,0,0.08)', 406 }, 407 408 // === Z-INDEX === 409 zIndex: { 410 dropdown: '100', 411 sticky: '200', 412 fixed: '300', 413 'modal-backdrop': '400', 414 modal: '500', 415 popover: '600', 416 tooltip: '700', 417 toast: '800', 418 max: '9999', 419 }, 420 421 // === TRANSITIONS === 422 transitionDuration: { 423 instant: '0ms', 424 fast: '150ms', 425 normal: '200ms', 426 slow: '300ms', 427 }, 428 429 // === ANIMATIONS === 430 keyframes: { 431 pulse: { 432 '0%, 100%': { opacity: '1' }, 433 '50%': { opacity: '0.5' }, 434 }, 435 'fade-in': { 436 '0%': { opacity: '0', transform: 'translateY(8px)' }, 437 '100%': { opacity: '1', transform: 'translateY(0)' }, 438 }, 439 }, 440 animation: { 441 pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', 442 'fade-in': 'fade-in 0.3s ease-out', 443 }, 444 }, 445 }, 446 447 plugins: [], 448 }; 449 `; 450 } 451 452 // ============================================================================= 453 // Write Output Files 454 // ============================================================================= 455 456 // Generate and write CSS (to both dist/ and src/styles/) 457 const cssContent = generateCSS(); 458 fs.writeFileSync(path.join(DIST_DIR, 'tokens.css'), cssContent); 459 fs.writeFileSync(path.join(STYLES_DIR, 'generated-tokens.css'), cssContent); 460 console.log('ā Generated: dist/tokens.css'); 461 console.log('ā Generated: src/styles/generated-tokens.css'); 462 463 // Generate and write TypeScript 464 const tsContent = generateTypeScript(); 465 fs.writeFileSync(path.join(DIST_DIR, 'tokens.ts'), tsContent); 466 console.log('ā Generated: dist/tokens.ts'); 467 468 // Generate and write Tailwind preset 469 const tailwindContent = generateTailwindPreset(); 470 fs.writeFileSync(path.join(ROOT, 'tailwind.preset.js'), tailwindContent); 471 console.log('ā Generated: tailwind.preset.js'); 472 473 console.log('\nš Token build complete!'); 474 console.log('\nOutputs:'); 475 console.log(' - dist/tokens.css (CSS custom properties)'); 476 console.log(' - src/styles/generated-tokens.css (for build-time import)'); 477 console.log(' - dist/tokens.ts (TypeScript constants)'); 478 console.log(' - tailwind.preset.js (Tailwind configuration)');