/ scripts / build-tokens.js
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)');