learning_ai_common_plat/packages/design-tokens/scripts/generate.ts
saravanakumardb1 09c767295a feat(design-tokens): generate platform token files (CSS, TS, Kotlin, Swift)
Updated generator to match existing MindLyst conventions:
- 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] selectors, --ml-fs-*, --ml-elevation-*, --ml-motion-*, light theme

Generated 4 files:
- generated/tokens.css — CSS custom properties (dark + light themes)
- generated/tokens.ts — Full token tree as const
- generated/MindLystTokens.kt — KMP shared module (drop-in for existing)
- generated/MindLystTheme.swift — SwiftUI structs (drop-in for existing)
2026-02-12 12:15:07 -08:00

331 lines
14 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 { 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/");