inou/design/generate.js

287 lines
10 KiB
JavaScript

#!/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();