learning_ai_common_plat/packages/design-tokens/scripts/generate.ts
saravanakumardb1 6b6f147de7 feat(design-tokens): extend generator with per-product Swift + Kotlin native themes
- Add generateProductSwift() and generateProductKotlin() to generate.ts
- Add PRODUCT_NATIVE_MAP for 10 products (ChronoMind, JarvisJr, PeakPulse, LysnrAI, NomGap, ActionTrail, FlowMonk, NoteLett, LocalMemGPT, LocalLLMLab)
- Output 20 native token files in generated/native/
- Fix TS type narrowing for gradient objects (line 382)
- Update DESIGN_SYSTEM_AUDIT.md with Appendix G remediation results
2026-03-28 00:25:03 -07:00

749 lines
29 KiB
TypeScript

/**
* Token generator — reads bytelyst.tokens.json, outputs 4 platform formats:
* 1. CSS custom properties (tokens.css)
* 2. TypeScript constants (tokens.ts)
* 3. Kotlin object (MindLystTokens.kt) — for KMP shared module
* 4. Swift structs (MindLystTheme.swift) — for iOS SwiftUI
*
* Output conventions match the hand-written originals in learning_multimodal_memory_agents:
* - Kotlin: SCREAMING_SNAKE_CASE, Palette/Dark/Light/BrainGradient/Typography/Motion/Layout
* - Swift: Color(hex: UInt), dark/light prefixes, Gradient(colors:), MindLystMotion, Color ext
* - CSS: [data-theme], --ml-fs-*, --ml-elevation-*, --ml-motion-*
*
* Usage: tsx scripts/generate.ts
*/
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const tokensPath = resolve(__dirname, '../tokens/bytelyst.tokens.json');
const outDir = resolve(__dirname, '../generated');
mkdirSync(outDir, { recursive: true });
const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'));
// ── Product CSS mapping ─────────────────────────────────────────────
const PRODUCT_CSS_MAP: Record<string, { prefix: string; colorsKey: string }> = {
mindlyst: { prefix: 'ml', colorsKey: 'brain' },
chronomind: { prefix: 'cm', colorsKey: 'chronomind' },
jarvisjr: { prefix: 'jj', colorsKey: 'jarvisjr' },
nomgap: { prefix: 'ng', colorsKey: 'nomgap' },
actiontrail: { prefix: 'at', colorsKey: 'actiontrail' },
flowmonk: { prefix: 'fm', colorsKey: 'flowmonk' },
notelett: { prefix: 'nl', colorsKey: 'notelett' },
localmemgpt: { prefix: 'lmg', colorsKey: 'localmemgpt' },
localllmlab: { prefix: 'llm', colorsKey: 'localllmlab' },
lysnrai: { prefix: 'lys', colorsKey: 'lysnrai' },
peakpulse: { prefix: 'pp', colorsKey: 'peakpulse' },
};
// ── Helpers ──────────────────────────────────────────────────────────
function camelToKebab(str: string): string {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
function camelToScreamingSnake(str: string): string {
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
}
function hexToUInt(hex: string): string {
return `0x${hex.replace('#', '').toUpperCase()}`;
}
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
// ── 1. CSS ───────────────────────────────────────────────────────────
function generateCSS(): string {
const lines: string[] = [
'/* Auto-generated from bytelyst.tokens.json — do not edit manually */',
'',
':root,',
'[data-theme="dark"] {',
];
// Semantic colors (dark theme as default)
for (const [key, value] of Object.entries(tokens.color.semantic.dark)) {
lines.push(` --ml-${camelToKebab(key)}: ${value};`);
}
lines.push('');
// Typography
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
// Swap single quotes → double quotes for CSS
const cssVal = typeof value === 'string' ? value.replace(/'/g, '"') : value;
lines.push(` --ml-font-${key}: ${cssVal};`);
}
lines.push('');
// Font sizes (--ml-fs-* to match existing convention)
for (const [key, value] of Object.entries(tokens.typography.fontSize)) {
lines.push(` --ml-fs-${key}: ${value}px;`);
}
lines.push('');
// Spacing
for (const [key, value] of Object.entries(tokens.spacing)) {
lines.push(` --ml-space-${key}: ${value === 0 ? '0' : `${value}px`};`);
}
lines.push('');
// Radius
for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` --ml-radius-${key}: ${value}px;`);
}
lines.push('');
// Elevation (--ml-elevation-* to match existing)
for (const [key, value] of Object.entries(tokens.elevation)) {
if (key === 'none') continue;
lines.push(` --ml-elevation-${key}: ${value};`);
}
lines.push('');
// Motion
for (const [key, value] of Object.entries(tokens.motion.duration)) {
if (key === 'instant') continue; // not used in CSS
lines.push(` --ml-motion-${key}: ${value}ms;`);
}
lines.push(` --ml-easing-standard: ${tokens.motion.easing.standard};`);
lines.push('}', '');
// Light theme overrides
lines.push('[data-theme="light"] {');
for (const [key, value] of Object.entries(tokens.color.semantic.light)) {
// Only emit overrides where light differs from dark
const darkVal = tokens.color.semantic.dark[key];
if (value !== darkVal) {
lines.push(` --ml-${camelToKebab(key)}: ${value};`);
}
}
lines.push('}', '');
return lines.join('\n');
}
// ── 2. TypeScript ────────────────────────────────────────────────────
function generateTS(): string {
return [
'// Auto-generated from bytelyst.tokens.json — do not edit manually',
'',
`export const tokens = ${JSON.stringify(tokens, null, 2)} as const;`,
'',
'export type Tokens = typeof tokens;',
'',
].join('\n');
}
// ── 3. Kotlin ────────────────────────────────────────────────────────
function generateKotlin(): string {
const lines: string[] = [
'// Auto-generated from bytelyst.tokens.json — do not edit manually',
'package com.mindlyst.shared.theme',
'',
'/**',
' * Cross-platform design tokens from bytelyst.tokens.json.',
' * Single source of truth consumed by both Android (Compose) and iOS (SwiftUI).',
' */',
'object MindLystTokens {',
'',
];
// ── Palette
lines.push(' // ── Color Palette ────────────────────────────────────────────────');
lines.push(' object Palette {');
for (const [key, value] of Object.entries(tokens.color.palette.neutral)) {
lines.push(
` const val NEUTRAL_${key} = 0xFF${(value as string).replace('#', '').toUpperCase()}`
);
}
lines.push('');
for (const [key, value] of Object.entries(tokens.color.palette.brand)) {
lines.push(
` const val ${key.toUpperCase()} = 0xFF${(value as string).replace('#', '').toUpperCase()}`
);
}
lines.push(' }', '');
// ── Dark semantic
lines.push(' // ── Semantic Colors (Dark Theme) ─────────────────────────────────');
lines.push(' object Dark {');
for (const [key, value] of Object.entries(tokens.color.semantic.dark)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(
` const val ${camelToScreamingSnake(key)} = 0xFF${value.replace('#', '').toUpperCase()}`
);
}
}
lines.push(' }', '');
// ── Light semantic
lines.push(' // ── Semantic Colors (Light Theme) ────────────────────────────────');
lines.push(' object Light {');
for (const [key, value] of Object.entries(tokens.color.semantic.light)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(
` const val ${camelToScreamingSnake(key)} = 0xFF${value.replace('#', '').toUpperCase()}`
);
}
}
lines.push(' }', '');
// ── Brain gradients
lines.push(' // ── Brain Identity Gradients ─────────────────────────────────────');
lines.push(' data class BrainGradient(val from: Long, val to: Long)');
lines.push('');
for (const [name, grad] of Object.entries(tokens.color.brain) as [
string,
{ from: string; to: string },
][]) {
lines.push(
` val BRAIN_${name.toUpperCase()} = BrainGradient(from = 0xFF${grad.from.replace('#', '').toUpperCase()}, to = 0xFF${grad.to.replace('#', '').toUpperCase()})`
);
}
lines.push('');
// ── Spacing
lines.push(' // ── Spacing (8pt grid) ───────────────────────────────────────────');
lines.push(' object Spacing {');
for (const [key, value] of Object.entries(tokens.spacing)) {
lines.push(` const val X${key} = ${value}`);
}
lines.push(' }', '');
// ── Radius
lines.push(' // ── Radius ───────────────────────────────────────────────────────');
lines.push(' object Radius {');
for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` const val ${key.toUpperCase()} = ${value}`);
}
lines.push(' }', '');
// ── Typography
lines.push(' // ── Typography ───────────────────────────────────────────────────');
lines.push(' object Typography {');
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
// Extract just the primary font name (first in the list)
const fontName =
typeof value === 'string' ? value.split(',')[0].replace(/'/g, '').trim() : value;
lines.push(` const val FONT_${key.toUpperCase()} = "${fontName}"`);
}
lines.push('');
for (const [key, value] of Object.entries(tokens.typography.fontSize)) {
const sizeKey = key.toUpperCase().replace('-', '');
lines.push(` const val SIZE_${sizeKey} = ${value}`);
}
lines.push(' }', '');
// ── Motion
lines.push(' // ── Motion ───────────────────────────────────────────────────────');
lines.push(' object Motion {');
for (const [key, value] of Object.entries(tokens.motion.duration)) {
lines.push(` const val ${key.toUpperCase()} = ${value}`);
}
lines.push(' }', '');
// ── Layout
lines.push(' // ── Layout ───────────────────────────────────────────────────────');
lines.push(' object Layout {');
lines.push(` const val TOUCH_TARGET_MIN = ${tokens.layout.touchTargetMin}`);
lines.push(` const val MOBILE_GUTTER = ${tokens.layout.mobileGutter}`);
lines.push(` const val MAX_WIDTH = ${tokens.layout.maxContentWidth}`);
lines.push(' }');
lines.push('}', '');
return lines.join('\n');
}
// ── 4. Swift ─────────────────────────────────────────────────────────
function generateSwift(): string {
const lines: string[] = [
'// Auto-generated from bytelyst.tokens.json — do not edit manually',
'import SwiftUI',
'',
'// MARK: - MindLyst Design Tokens (from shared KMP MindLystTokens)',
'// These values mirror MindLystTokens.kt exactly.',
'',
'struct MindLystColors {',
];
// Dark colors
lines.push(' // Dark');
for (const [key, value] of Object.entries(tokens.color.semantic.dark)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(` static let dark${capitalize(key)} = Color(hex: ${hexToUInt(value)})`);
} else if (typeof value === 'string' && value.startsWith('rgba')) {
// border/overlay → special handling
if (key === 'borderDefault') {
lines.push(' static let darkBorder = Color.white.opacity(0.12)');
}
}
}
lines.push('');
// Light colors
lines.push(' // Light');
for (const [key, value] of Object.entries(tokens.color.semantic.light)) {
if (typeof value === 'string' && value.startsWith('#')) {
if (value === '#FFFFFF') {
lines.push(` static let light${capitalize(key)} = Color.white`);
} else {
lines.push(` static let light${capitalize(key)} = Color(hex: ${hexToUInt(value)})`);
}
}
}
lines.push('');
// Brain gradients
lines.push(' // Brain Gradients');
for (const [name, grad] of Object.entries(tokens.color.brain) as [
string,
{ from: string; to: string },
][]) {
lines.push(
` static let brain${capitalize(name)} = Gradient(colors: [Color(hex: ${hexToUInt(grad.from)}), Color(hex: ${hexToUInt(grad.to)})])`
);
}
lines.push('}', '');
// Spacing
lines.push('struct MindLystSpacing {');
for (const [key, value] of Object.entries(tokens.spacing)) {
const pad = key.length === 1 ? ' ' : '';
lines.push(` static let x${key}: ${pad}CGFloat = ${value}`);
}
lines.push('}', '');
// Radius
lines.push('struct MindLystRadius {');
for (const [key, value] of Object.entries(tokens.radius)) {
const pad = key.length < 4 ? ' '.repeat(4 - key.length) : '';
lines.push(` static let ${key}:${pad} CGFloat = ${value}`);
}
lines.push('}', '');
// Motion (durations in seconds)
lines.push('struct MindLystMotion {');
for (const [key, value] of Object.entries(tokens.motion.duration)) {
const seconds = (value as number) / 1000;
const pad = key.length < 7 ? ' '.repeat(7 - key.length) : '';
lines.push(` static let ${key}:${pad} Double = ${seconds.toFixed(2)}`);
}
lines.push('}', '');
// Color hex extension
lines.push('// MARK: - Color Hex Extension');
lines.push('');
lines.push('extension Color {');
lines.push(' init(hex: UInt, alpha: Double = 1.0) {');
lines.push(' self.init(');
lines.push(' .sRGB,');
lines.push(' red: Double((hex >> 16) & 0xFF) / 255.0,');
lines.push(' green: Double((hex >> 8) & 0xFF) / 255.0,');
lines.push(' blue: Double(hex & 0xFF) / 255.0,');
lines.push(' opacity: alpha');
lines.push(' )');
lines.push(' }');
lines.push('}', '');
return lines.join('\n');
}
// ── 5. Per-product CSS ──────────────────────────────────────────────
function generateProductCSS(productId: string, prefix: string, colorsKey: string): string {
const productColors = tokens.color[colorsKey];
if (!productColors) return `/* No palette found for ${productId} (color.${colorsKey}) */\n`;
const lines: string[] = [
`/* Auto-generated ${productId} tokens from bytelyst.tokens.json — do not edit manually */`,
'',
':root {',
];
// Semantic colors (dark as default) — shared across all products
for (const [key, value] of Object.entries(tokens.color.semantic.dark)) {
lines.push(` --${prefix}-${camelToKebab(key)}: ${value};`);
}
lines.push('');
// Product-specific colors
lines.push(` /* ${productId} product colors */`);
for (const [key, value] of Object.entries(productColors)) {
if (typeof value === 'string') {
lines.push(` --${prefix}-${camelToKebab(key)}: ${value};`);
} else if (typeof value === 'object' && value !== null && 'from' in value && 'to' in value) {
const grad = value as unknown as { from: string; to: string };
lines.push(` --${prefix}-${camelToKebab(key)}-from: ${grad.from};`);
lines.push(` --${prefix}-${camelToKebab(key)}-to: ${grad.to};`);
}
}
lines.push('');
// Typography
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
const cssVal = typeof value === 'string' ? value.replace(/'/g, '"') : value;
lines.push(` --${prefix}-font-${key}: ${cssVal};`);
}
lines.push('');
// Font sizes
for (const [key, value] of Object.entries(tokens.typography.fontSize)) {
lines.push(` --${prefix}-fs-${key}: ${value}px;`);
}
lines.push('');
// Spacing
for (const [key, value] of Object.entries(tokens.spacing)) {
lines.push(` --${prefix}-space-${key}: ${value === 0 ? '0' : `${value}px`};`);
}
lines.push('');
// Radius
for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` --${prefix}-radius-${key}: ${value}px;`);
}
lines.push('');
// Elevation
for (const [key, value] of Object.entries(tokens.elevation)) {
if (key === 'none') continue;
lines.push(` --${prefix}-elevation-${key}: ${value};`);
}
lines.push('');
// Motion
for (const [key, value] of Object.entries(tokens.motion.duration)) {
if (key === 'instant') continue;
lines.push(` --${prefix}-motion-${key}: ${value}ms;`);
}
lines.push(` --${prefix}-easing-standard: ${tokens.motion.easing.standard};`);
lines.push('}', '');
// Light theme overrides
lines.push('[data-theme="light"] {');
for (const [key, value] of Object.entries(tokens.color.semantic.light)) {
const darkVal = (tokens.color.semantic.dark as Record<string, unknown>)[key];
if (value !== darkVal) {
lines.push(` --${prefix}-${camelToKebab(key)}: ${value};`);
}
}
lines.push('}', '');
return lines.join('\n');
}
// ── Product native mapping (products with iOS/Android apps) ─────────
interface ProductNativeConfig {
colorsKey: string;
swiftEnum: string; // e.g. 'PeakPulseColors'
kotlinObject: string; // e.g. 'PeakPulseTokens'
kotlinPackage: string; // e.g. 'com.peakpulse.theme'
swiftFile: string; // e.g. 'PeakPulseTheme.swift'
kotlinFile: string; // e.g. 'PeakPulseTokens.kt'
}
const PRODUCT_NATIVE_MAP: Record<string, ProductNativeConfig> = {
chronomind: {
colorsKey: 'chronomind',
swiftEnum: 'CMColors',
kotlinObject: 'ChronoMindTokens',
kotlinPackage: 'com.chronomind.app.theme',
swiftFile: 'ChronoMindTheme.generated.swift',
kotlinFile: 'ChronoMindTokens.generated.kt',
},
jarvisjr: {
colorsKey: 'jarvisjr',
swiftEnum: 'JarvisJrColors',
kotlinObject: 'JarvisJrTokens',
kotlinPackage: 'com.jarvisjr.app.theme',
swiftFile: 'JarvisJrTheme.generated.swift',
kotlinFile: 'JarvisJrTokens.generated.kt',
},
peakpulse: {
colorsKey: 'peakpulse',
swiftEnum: 'PeakPulseColors',
kotlinObject: 'PeakPulseTokens',
kotlinPackage: 'com.peakpulse.theme',
swiftFile: 'PeakPulseTheme.generated.swift',
kotlinFile: 'PeakPulseTokens.generated.kt',
},
lysnrai: {
colorsKey: 'lysnrai',
swiftEnum: 'LysnrAIColors',
kotlinObject: 'LysnrAITokens',
kotlinPackage: 'com.saravana.lysnrai.theme',
swiftFile: 'LysnrAITheme.generated.swift',
kotlinFile: 'LysnrAITokens.generated.kt',
},
nomgap: {
colorsKey: 'nomgap',
swiftEnum: 'NomGapColors',
kotlinObject: 'NomGapTokens',
kotlinPackage: 'com.nomgap.theme',
swiftFile: 'NomGapTheme.generated.swift',
kotlinFile: 'NomGapTokens.generated.kt',
},
actiontrail: {
colorsKey: 'actiontrail',
swiftEnum: 'ActionTrailColors',
kotlinObject: 'ActionTrailTokens',
kotlinPackage: 'com.actiontrail.theme',
swiftFile: 'ActionTrailTheme.generated.swift',
kotlinFile: 'ActionTrailTokens.generated.kt',
},
flowmonk: {
colorsKey: 'flowmonk',
swiftEnum: 'FlowMonkColors',
kotlinObject: 'FlowMonkTokens',
kotlinPackage: 'com.flowmonk.theme',
swiftFile: 'FlowMonkTheme.generated.swift',
kotlinFile: 'FlowMonkTokens.generated.kt',
},
notelett: {
colorsKey: 'notelett',
swiftEnum: 'NoteLettColors',
kotlinObject: 'NoteLettTokens',
kotlinPackage: 'com.notelett.theme',
swiftFile: 'NoteLettTheme.generated.swift',
kotlinFile: 'NoteLettTokens.generated.kt',
},
localmemgpt: {
colorsKey: 'localmemgpt',
swiftEnum: 'LocalMemGPTColors',
kotlinObject: 'LocalMemGPTTokens',
kotlinPackage: 'com.localmemgpt.theme',
swiftFile: 'LocalMemGPTTheme.generated.swift',
kotlinFile: 'LocalMemGPTTokens.generated.kt',
},
localllmlab: {
colorsKey: 'localllmlab',
swiftEnum: 'LocalLLMLabColors',
kotlinObject: 'LocalLLMLabTokens',
kotlinPackage: 'com.localllmlab.theme',
swiftFile: 'LocalLLMLabTheme.generated.swift',
kotlinFile: 'LocalLLMLabTokens.generated.kt',
},
};
// ── 6. Per-product Swift ─────────────────────────────────────────────
function generateProductSwift(productId: string, config: ProductNativeConfig): string {
const productColors = tokens.color[config.colorsKey];
const lines: string[] = [
`// Auto-generated from bytelyst.tokens.json — do not edit manually.`,
`// Product: ${productId}`,
`// Regenerate: cd packages/design-tokens && tsx scripts/generate.ts`,
'',
'import SwiftUI',
'',
`enum ${config.swiftEnum} {`,
];
// Semantic dark colors
lines.push(' // MARK: - Semantic (Dark Theme)');
for (const [key, value] of Object.entries(tokens.color.semantic.dark)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(` static let ${key} = Color(hex: ${hexToUInt(value)})`);
}
}
lines.push('');
// Product-specific colors
if (productColors) {
lines.push(` // MARK: - ${capitalize(productId)} Product Colors`);
for (const [key, value] of Object.entries(productColors)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(` static let ${key} = Color(hex: ${hexToUInt(value)})`);
} else if (typeof value === 'object' && value !== null && 'from' in value) {
const grad = value as { from: string; to: string };
lines.push(
` static let ${key} = Gradient(colors: [Color(hex: ${hexToUInt(grad.from)}), Color(hex: ${hexToUInt(grad.to)})])`
);
}
}
lines.push('');
}
lines.push('}', '');
// Semantic light colors
lines.push(`enum ${config.swiftEnum}Light {`);
lines.push(' // MARK: - Semantic (Light Theme)');
for (const [key, value] of Object.entries(tokens.color.semantic.light)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(` static let ${key} = Color(hex: ${hexToUInt(value)})`);
}
}
lines.push('}', '');
// Spacing
lines.push(`enum ${config.swiftEnum.replace('Colors', '')}Spacing {`);
for (const [key, value] of Object.entries(tokens.spacing)) {
const pad = key.length === 1 ? ' ' : '';
lines.push(` static let x${key}: ${pad}CGFloat = ${value}`);
}
lines.push('}', '');
// Radius
lines.push(`enum ${config.swiftEnum.replace('Colors', '')}Radius {`);
for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` static let ${key}: CGFloat = ${value}`);
}
lines.push('}', '');
// Motion
lines.push(`enum ${config.swiftEnum.replace('Colors', '')}Motion {`);
for (const [key, value] of Object.entries(tokens.motion.duration)) {
const seconds = (value as number) / 1000;
lines.push(` static let ${key}: Double = ${seconds.toFixed(2)}`);
}
lines.push('}', '');
// Color hex extension (only include if not already in project)
lines.push('// MARK: - Color Hex Extension (import if not already defined)');
lines.push('');
lines.push('extension Color {');
lines.push(' init(hex: UInt, alpha: Double = 1.0) {');
lines.push(' self.init(');
lines.push(' .sRGB,');
lines.push(' red: Double((hex >> 16) & 0xFF) / 255.0,');
lines.push(' green: Double((hex >> 8) & 0xFF) / 255.0,');
lines.push(' blue: Double(hex & 0xFF) / 255.0,');
lines.push(' opacity: alpha');
lines.push(' )');
lines.push(' }');
lines.push('}', '');
return lines.join('\n');
}
// ── 7. Per-product Kotlin ────────────────────────────────────────────
function generateProductKotlin(productId: string, config: ProductNativeConfig): string {
const productColors = tokens.color[config.colorsKey];
const lines: string[] = [
'// Auto-generated from bytelyst.tokens.json — do not edit manually.',
`// Product: ${productId}`,
'// Regenerate: cd packages/design-tokens && tsx scripts/generate.ts',
`package ${config.kotlinPackage}`,
'',
`object ${config.kotlinObject} {`,
'',
];
// Semantic dark
lines.push(' // ── Semantic Colors (Dark Theme) ─────────────────────────────────');
lines.push(' object Dark {');
for (const [key, value] of Object.entries(tokens.color.semantic.dark)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(
` const val ${camelToScreamingSnake(key)} = 0xFF${value.replace('#', '').toUpperCase()}`
);
}
}
lines.push(' }', '');
// Semantic light
lines.push(' // ── Semantic Colors (Light Theme) ────────────────────────────────');
lines.push(' object Light {');
for (const [key, value] of Object.entries(tokens.color.semantic.light)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(
` const val ${camelToScreamingSnake(key)} = 0xFF${value.replace('#', '').toUpperCase()}`
);
}
}
lines.push(' }', '');
// Product-specific colors
if (productColors) {
lines.push(` // ── ${capitalize(productId)} Product Colors ───────────────────────────────`);
lines.push(' object Product {');
for (const [key, value] of Object.entries(productColors)) {
if (typeof value === 'string' && value.startsWith('#')) {
lines.push(
` const val ${camelToScreamingSnake(key)} = 0xFF${value.replace('#', '').toUpperCase()}`
);
} else if (typeof value === 'object' && value !== null && 'from' in value) {
const grad = value as { from: string; to: string };
lines.push(
` const val ${camelToScreamingSnake(key)}_FROM = 0xFF${grad.from.replace('#', '').toUpperCase()}`
);
lines.push(
` const val ${camelToScreamingSnake(key)}_TO = 0xFF${grad.to.replace('#', '').toUpperCase()}`
);
}
}
lines.push(' }', '');
}
// Spacing
lines.push(' // ── Spacing (8pt grid) ───────────────────────────────────────────');
lines.push(' object Spacing {');
for (const [key, value] of Object.entries(tokens.spacing)) {
lines.push(` const val X${key} = ${value}`);
}
lines.push(' }', '');
// Radius
lines.push(' // ── Radius ───────────────────────────────────────────────────────');
lines.push(' object Radius {');
for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` const val ${key.toUpperCase()} = ${value}`);
}
lines.push(' }', '');
// Typography
lines.push(' // ── Typography ───────────────────────────────────────────────────');
lines.push(' object Typography {');
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
const fontName =
typeof value === 'string' ? value.split(',')[0].replace(/'/g, '').trim() : value;
lines.push(` const val FONT_${key.toUpperCase()} = "${fontName}"`);
}
lines.push(' }', '');
// Motion
lines.push(' // ── Motion ───────────────────────────────────────────────────────');
lines.push(' object Motion {');
for (const [key, value] of Object.entries(tokens.motion.duration)) {
lines.push(` const val ${key.toUpperCase()} = ${value}`);
}
lines.push(' }');
lines.push('}', '');
return lines.join('\n');
}
// ── Write all ────────────────────────────────────────────────────────
// Shared semantic tokens (backward compatible)
writeFileSync(resolve(outDir, 'tokens.css'), generateCSS());
writeFileSync(resolve(outDir, 'tokens.ts'), generateTS());
writeFileSync(resolve(outDir, 'MindLystTokens.kt'), generateKotlin());
writeFileSync(resolve(outDir, 'MindLystTheme.swift'), generateSwift());
// Per-product CSS
for (const [productId, config] of Object.entries(PRODUCT_CSS_MAP)) {
const css = generateProductCSS(productId, config.prefix, config.colorsKey);
writeFileSync(resolve(outDir, `${productId}.css`), css);
}
// Per-product Swift + Kotlin
const nativeDir = resolve(outDir, 'native');
mkdirSync(nativeDir, { recursive: true });
for (const [productId, config] of Object.entries(PRODUCT_NATIVE_MAP)) {
const swift = generateProductSwift(productId, config);
writeFileSync(resolve(nativeDir, config.swiftFile), swift);
const kotlin = generateProductKotlin(productId, config);
writeFileSync(resolve(nativeDir, config.kotlinFile), kotlin);
}
console.log(
`Generated 4 shared + ${Object.keys(PRODUCT_CSS_MAP).length} product CSS + ${Object.keys(PRODUCT_NATIVE_MAP).length * 2} native token files in generated/`
);