#!/usr/bin/env node /** * Design Token Generator * Single source of truth → CSS variables + Flutter theme * * Usage: node generate.js * Outputs: * - tokens.css (CSS custom properties) * - flutter/inou_theme.dart (Flutter ThemeData) */ const fs = require('fs'); const path = require('path'); const tokens = JSON.parse(fs.readFileSync(path.join(__dirname, 'tokens.json'), 'utf8')); // ============================================ // CSS GENERATOR // ============================================ function generateCSS(tokens) { const lines = [ '/* AUTO-GENERATED from tokens.json — do not edit directly */', '/* Run: node design/generate.js */', '', ':root {' ]; // Colors for (const [key, value] of Object.entries(tokens.colors)) { const cssVar = `--${camelToKebab(key)}`; lines.push(` ${cssVar}: ${value};`); } lines.push(''); // Spacing for (const [key, value] of Object.entries(tokens.spacing.scale)) { lines.push(` --space-${key}: ${value}px;`); } lines.push(''); // Radii for (const [key, value] of Object.entries(tokens.radii)) { if (key !== 'full') { lines.push(` --radius-${key}: ${value}px;`); } else { lines.push(` --radius-${key}: ${value}px;`); } } lines.push(''); // Layout lines.push(` --max-width: ${tokens.layout.maxWidth}px;`); lines.push(` --max-width-narrow: ${tokens.layout.maxWidthNarrow}px;`); lines.push(` --max-width-form: ${tokens.layout.maxWidthForm}px;`); lines.push('}'); return lines.join('\n'); } // ============================================ // FLUTTER GENERATOR // ============================================ function generateFlutter(tokens) { const lines = [ '// AUTO-GENERATED from tokens.json — do not edit directly', '// Run: node design/generate.js', '', "import 'package:flutter/material.dart';", "import 'package:google_fonts/google_fonts.dart';", '', '/// inou Design System', '/// Single source of truth: design/tokens.json', 'class InouTheme {', ' InouTheme._();', '', ' // ============================================', ' // COLORS', ' // ============================================', ]; // Colors as static constants for (const [key, value] of Object.entries(tokens.colors)) { const hex = value.replace('#', ''); const alpha = hex.length === 6 ? 'FF' : ''; lines.push(` static const Color ${key} = Color(0x${alpha}${hex.toUpperCase()});`); } lines.push(''); lines.push(' // Indicator colors (data sections)'); for (const [key, value] of Object.entries(tokens.indicators)) { const hex = value.replace('#', ''); lines.push(` static const Color indicator${capitalize(key)} = Color(0xFF${hex.toUpperCase()});`); } // Spacing lines.push(''); lines.push(' // ============================================'); lines.push(' // SPACING'); lines.push(' // ============================================'); for (const [key, value] of Object.entries(tokens.spacing.scale)) { lines.push(` static const double space${capitalize(key)} = ${value}.0;`); } // Radii lines.push(''); lines.push(' // ============================================'); lines.push(' // BORDER RADIUS'); lines.push(' // ============================================'); for (const [key, value] of Object.entries(tokens.radii)) { lines.push(` static const double radius${capitalize(key)} = ${value}.0;`); } lines.push(` static BorderRadius get borderRadiusSm => BorderRadius.circular(radiusSm);`); lines.push(` static BorderRadius get borderRadiusMd => BorderRadius.circular(radiusMd);`); lines.push(` static BorderRadius get borderRadiusLg => BorderRadius.circular(radiusLg);`); // Layout lines.push(''); lines.push(' // ============================================'); lines.push(' // LAYOUT'); lines.push(' // ============================================'); lines.push(` static const double maxWidth = ${tokens.layout.maxWidth}.0;`); lines.push(` static const double maxWidthNarrow = ${tokens.layout.maxWidthNarrow}.0;`); lines.push(` static const double maxWidthForm = ${tokens.layout.maxWidthForm}.0;`); // Typography helpers lines.push(''); lines.push(' // ============================================'); lines.push(' // TYPOGRAPHY'); lines.push(' // ============================================'); lines.push(` static String get fontFamily => '${tokens.typography.fontFamily}';`); lines.push(''); // Text styles const textStyles = { h1: { size: 36, weight: 300, spacing: -0.5 }, h1Large: { size: 40, weight: 700 }, h2: { size: 24, weight: 300, spacing: -0.3 }, h3: { size: 18, weight: 500 }, bodyLarge: { size: 16, weight: 400 }, bodyMedium: { size: 15, weight: 400 }, bodySmall: { size: 13, weight: 400 }, labelLarge: { size: 15, weight: 500 }, labelSmall: { size: 12, weight: 500, spacing: 1.5 }, }; for (const [name, style] of Object.entries(textStyles)) { const weight = `FontWeight.w${style.weight}`; const spacing = style.spacing ? `, letterSpacing: ${style.spacing}` : ''; lines.push(` static TextStyle get ${name} => GoogleFonts.sora(`); lines.push(` fontSize: ${style.size}.0,`); lines.push(` fontWeight: ${weight},`); lines.push(` color: text${spacing},`); lines.push(` );`); lines.push(''); } // Color scheme lines.push(' // ============================================'); lines.push(' // THEME DATA'); lines.push(' // ============================================'); lines.push(' static ThemeData get light => ThemeData('); lines.push(' useMaterial3: true,'); lines.push(' brightness: Brightness.light,'); lines.push(' scaffoldBackgroundColor: bg,'); lines.push(' colorScheme: ColorScheme.light('); lines.push(' primary: accent,'); lines.push(' onPrimary: Colors.white,'); lines.push(' secondary: accentLight,'); lines.push(' onSecondary: accent,'); lines.push(' surface: bgCard,'); lines.push(' onSurface: text,'); lines.push(' error: danger,'); lines.push(' onError: Colors.white,'); lines.push(' outline: border,'); lines.push(' ),'); lines.push(' textTheme: TextTheme('); lines.push(' displayLarge: h1Large,'); lines.push(' displayMedium: h1,'); lines.push(' headlineMedium: h2,'); lines.push(' headlineSmall: h3,'); lines.push(' bodyLarge: bodyLarge,'); lines.push(' bodyMedium: bodyMedium,'); lines.push(' bodySmall: bodySmall,'); lines.push(' labelLarge: labelLarge,'); lines.push(' labelSmall: labelSmall,'); lines.push(' ),'); lines.push(' appBarTheme: AppBarTheme('); lines.push(' backgroundColor: bg,'); lines.push(' foregroundColor: text,'); lines.push(' elevation: 0,'); lines.push(' centerTitle: false,'); lines.push(' ),'); lines.push(' cardTheme: CardTheme('); lines.push(' color: bgCard,'); lines.push(' elevation: 0,'); lines.push(' shape: RoundedRectangleBorder('); lines.push(' borderRadius: borderRadiusLg,'); lines.push(' side: BorderSide(color: border),'); lines.push(' ),'); lines.push(' ),'); lines.push(' elevatedButtonTheme: ElevatedButtonThemeData('); lines.push(' style: ElevatedButton.styleFrom('); lines.push(' backgroundColor: accent,'); lines.push(' foregroundColor: Colors.white,'); lines.push(' elevation: 0,'); lines.push(' padding: EdgeInsets.symmetric(horizontal: spaceLg, vertical: spaceMd),'); lines.push(' shape: RoundedRectangleBorder(borderRadius: borderRadiusMd),'); lines.push(' textStyle: labelLarge,'); lines.push(' ),'); lines.push(' ),'); lines.push(' outlinedButtonTheme: OutlinedButtonThemeData('); lines.push(' style: OutlinedButton.styleFrom('); lines.push(' foregroundColor: text,'); lines.push(' side: BorderSide(color: border),'); lines.push(' padding: EdgeInsets.symmetric(horizontal: spaceLg, vertical: spaceMd),'); lines.push(' shape: RoundedRectangleBorder(borderRadius: borderRadiusMd),'); lines.push(' textStyle: labelLarge,'); lines.push(' ),'); lines.push(' ),'); lines.push(' inputDecorationTheme: InputDecorationTheme('); lines.push(' filled: true,'); lines.push(' fillColor: bgCard,'); lines.push(' border: OutlineInputBorder('); lines.push(' borderRadius: borderRadiusMd,'); lines.push(' borderSide: BorderSide(color: border),'); lines.push(' ),'); lines.push(' enabledBorder: OutlineInputBorder('); lines.push(' borderRadius: borderRadiusMd,'); lines.push(' borderSide: BorderSide(color: border),'); lines.push(' ),'); lines.push(' focusedBorder: OutlineInputBorder('); lines.push(' borderRadius: borderRadiusMd,'); lines.push(' borderSide: BorderSide(color: accent, width: 2),'); lines.push(' ),'); lines.push(' contentPadding: EdgeInsets.symmetric(horizontal: spaceMd, vertical: spaceMd),'); lines.push(' ),'); lines.push(' dividerTheme: DividerThemeData('); lines.push(' color: border,'); lines.push(' thickness: 1,'); lines.push(' ),'); lines.push(' );'); lines.push('}'); return lines.join('\n'); } // ============================================ // UTILITIES // ============================================ function camelToKebab(str) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } // ============================================ // MAIN // ============================================ function main() { // Generate CSS const css = generateCSS(tokens); const cssPath = path.join(__dirname, 'tokens.css'); fs.writeFileSync(cssPath, css); console.log(`✓ Generated ${cssPath}`); // Generate Flutter const flutter = generateFlutter(tokens); const flutterDir = path.join(__dirname, 'flutter'); if (!fs.existsSync(flutterDir)) { fs.mkdirSync(flutterDir, { recursive: true }); } const flutterPath = path.join(flutterDir, 'inou_theme.dart'); fs.writeFileSync(flutterPath, flutter); console.log(`✓ Generated ${flutterPath}`); console.log('\nDesign tokens synced. Both CSS and Flutter use the same values.'); } main();