287 lines
10 KiB
JavaScript
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();
|