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