/** * 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 { readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { resolve, dirname } 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")); // ── 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"); } // ── Write all ──────────────────────────────────────────────────────── writeFileSync(resolve(outDir, "tokens.css"), generateCSS()); writeFileSync(resolve(outDir, "tokens.ts"), generateTS()); writeFileSync(resolve(outDir, "MindLystTokens.kt"), generateKotlin()); writeFileSync(resolve(outDir, "MindLystTheme.swift"), generateSwift()); console.log("Generated 4 token files in generated/");