/** * 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 */ /* eslint-disable no-console -- This generator is a CLI; console output confirms generated artifacts. */ 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 = { 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(''); // ByteLyst semantic aliases. These are the shared UI contract and map the // historical MindLyst token names into a product-neutral design system. lines.push(' --bl-bg-canvas: var(--ml-bg-canvas);'); lines.push(' --bl-bg-elevated: var(--ml-bg-elevated);'); lines.push(' --bl-surface-card: var(--ml-surface-card);'); lines.push(' --bl-surface-muted: var(--ml-surface-muted);'); lines.push(' --bl-surface-highlight: color-mix(in oklab, var(--ml-surface-muted) 82%, white);'); lines.push(' --bl-surface-overlay: color-mix(in oklab, var(--ml-bg-canvas) 88%, transparent);'); lines.push( ' --bl-input: color-mix(in oklab, var(--ml-surface-muted) 76%, var(--ml-bg-canvas));' ); lines.push(' --bl-border: var(--ml-border-default);'); lines.push(' --bl-border-strong: var(--ml-border-strong);'); lines.push( ' --bl-border-subtle: color-mix(in oklab, var(--ml-border-default) 62%, transparent);' ); lines.push(' --bl-text-primary: var(--ml-text-primary);'); lines.push(' --bl-text-secondary: var(--ml-text-secondary);'); lines.push(' --bl-text-tertiary: var(--ml-text-tertiary);'); lines.push( ' --bl-text-quiet: color-mix(in oklab, var(--ml-text-secondary) 78%, var(--ml-bg-canvas));' ); lines.push(' --bl-accent: var(--ml-accent-primary);'); lines.push(' --bl-accent-foreground: var(--ml-bg-canvas);'); lines.push( ' --bl-accent-muted: color-mix(in oklab, var(--ml-accent-primary) 16%, transparent);' ); lines.push(' --bl-info: var(--ml-accent-primary);'); lines.push(' --bl-info-muted: color-mix(in oklab, var(--ml-accent-primary) 14%, transparent);'); lines.push(' --bl-success: var(--ml-success);'); lines.push(' --bl-success-muted: color-mix(in oklab, var(--ml-success) 14%, transparent);'); lines.push(' --bl-warning: var(--ml-warning);'); lines.push(' --bl-warning-muted: color-mix(in oklab, var(--ml-warning) 14%, transparent);'); lines.push(' --bl-danger: var(--ml-danger);'); lines.push(' --bl-danger-muted: color-mix(in oklab, var(--ml-danger) 14%, transparent);'); lines.push(' --bl-danger-foreground: var(--ml-bg-canvas);'); lines.push(' --bl-focus-ring: var(--ml-focus-ring);'); lines.push( ' --bl-focus-ring-muted: color-mix(in oklab, var(--ml-accent-primary) 18%, transparent);' ); lines.push(' --bl-overlay-scrim: var(--ml-overlay-scrim);'); 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 — raw (--ml-*) tier, fixed pixel values for (const [key, value] of Object.entries(tokens.spacing)) { lines.push(` --ml-space-${key}: ${value === 0 ? '0' : `${value}px`};`); } lines.push(''); // Density-aware spacing tier (--bl-*) — added in design-tokens 0.2.0. // Each --bl-space-N = --ml-space-N × var(--bl-space-scale). // Consumers override --bl-space-scale via [data-density="..."] on // (or any ancestor) to switch between compact / comfortable / spacious. lines.push(' /* Density scale — overridden by [data-density] selectors below */'); lines.push(' --bl-space-scale: 1;'); for (const [key, value] of Object.entries(tokens.spacing)) { if (value === 0) { lines.push(` --bl-space-${key}: 0;`); } else { lines.push(` --bl-space-${key}: calc(var(--ml-space-${key}) * var(--bl-space-scale));`); } } lines.push(''); // Radius for (const [key, value] of Object.entries(tokens.radius)) { lines.push(` --ml-radius-${key}: ${value}px;`); } lines.push(' --bl-radius-control: var(--ml-radius-xs);'); lines.push(' --bl-radius-surface: var(--ml-radius-sm);'); lines.push(' --bl-radius-card: var(--ml-radius-md);'); lines.push(' --bl-radius-panel: var(--ml-radius-lg);'); lines.push(' --bl-radius-pill: var(--ml-radius-pill);'); 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(' --bl-shadow-sm: var(--ml-elevation-sm);'); lines.push(' --bl-shadow-md: var(--ml-elevation-md);'); lines.push(' --bl-shadow-lg: var(--ml-elevation-lg);'); 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('}', ''); // Density overrides — drive --bl-space-scale via [data-density="..."]. // Default (no attribute or `comfortable`) keeps scale = 1 from :root. // Added in design-tokens 0.2.0. lines.push('/* Density tier — overrides --bl-space-scale */'); lines.push('[data-density="compact"] { --bl-space-scale: 0.875; }'); lines.push('[data-density="comfortable"] { --bl-space-scale: 1; }'); lines.push('[data-density="spacious"] { --bl-space-scale: 1.125; }'); 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(''); // @bytelyst/ui alias layer — maps --bl-* → product --{prefix}-* so shared // components resolve the correct product-scoped tokens. lines.push(` /* ── @bytelyst/ui alias layer (--bl-* → --${prefix}-*) ──────────────── */`); lines.push(` --bl-bg-canvas: var(--${prefix}-bg-canvas);`); lines.push(` --bl-bg-elevated: var(--${prefix}-bg-elevated);`); lines.push(` --bl-surface-card: var(--${prefix}-surface-card);`); lines.push(` --bl-surface-muted: var(--${prefix}-surface-muted);`); lines.push( ` --bl-surface-highlight: color-mix(in oklab, var(--${prefix}-surface-muted) 82%, white);` ); lines.push( ` --bl-surface-overlay: color-mix(in oklab, var(--${prefix}-bg-canvas) 88%, transparent);` ); lines.push( ` --bl-input: color-mix(in oklab, var(--${prefix}-surface-muted) 76%, var(--${prefix}-bg-canvas));` ); lines.push(` --bl-border: var(--${prefix}-border-default);`); lines.push(` --bl-border-strong: var(--${prefix}-border-strong);`); lines.push( ` --bl-border-subtle: color-mix(in oklab, var(--${prefix}-border-default) 62%, transparent);` ); lines.push(` --bl-text-primary: var(--${prefix}-text-primary);`); lines.push(` --bl-text-secondary: var(--${prefix}-text-secondary);`); lines.push(` --bl-text-tertiary: var(--${prefix}-text-tertiary);`); lines.push( ` --bl-text-quiet: color-mix(in oklab, var(--${prefix}-text-secondary) 78%, var(--${prefix}-bg-canvas));` ); lines.push(` --bl-accent: var(--${prefix}-accent-primary);`); lines.push(` --bl-accent-foreground: var(--${prefix}-bg-canvas);`); lines.push( ` --bl-accent-muted: color-mix(in oklab, var(--${prefix}-accent-primary) 16%, transparent);` ); lines.push(` --bl-info: var(--${prefix}-accent-primary);`); lines.push( ` --bl-info-muted: color-mix(in oklab, var(--${prefix}-accent-primary) 14%, transparent);` ); lines.push(` --bl-success: var(--${prefix}-success);`); lines.push( ` --bl-success-muted: color-mix(in oklab, var(--${prefix}-success) 14%, transparent);` ); lines.push(` --bl-warning: var(--${prefix}-warning);`); lines.push( ` --bl-warning-muted: color-mix(in oklab, var(--${prefix}-warning) 14%, transparent);` ); lines.push(` --bl-danger: var(--${prefix}-danger);`); lines.push(` --bl-danger-muted: color-mix(in oklab, var(--${prefix}-danger) 14%, transparent);`); lines.push(` --bl-danger-foreground: var(--${prefix}-bg-canvas);`); lines.push(` --bl-focus-ring: var(--${prefix}-focus-ring);`); lines.push( ` --bl-focus-ring-muted: color-mix(in oklab, var(--${prefix}-accent-primary) 18%, transparent);` ); lines.push(` --bl-overlay-scrim: var(--${prefix}-overlay-scrim);`); 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)[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 = { 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/` );