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)
This commit is contained in:
saravanakumardb1 2026-02-12 12:15:07 -08:00
parent 4ae7a9d023
commit 09c767295a
5 changed files with 684 additions and 31 deletions

View File

@ -0,0 +1,88 @@
// 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
static let darkBgCanvas = Color(hex: 0x06070A)
static let darkBgElevated = Color(hex: 0x0E1118)
static let darkSurfaceCard = Color(hex: 0x121725)
static let darkSurfaceMuted = Color(hex: 0x1A2335)
static let darkBorder = Color.white.opacity(0.12)
static let darkTextPrimary = Color(hex: 0xEFF4FF)
static let darkTextSecondary = Color(hex: 0xA5B1C7)
static let darkTextTertiary = Color(hex: 0x6C7C98)
static let darkAccentPrimary = Color(hex: 0x5A8CFF)
static let darkAccentSecondary = Color(hex: 0x2EE6D6)
static let darkSuccess = Color(hex: 0x34D399)
static let darkWarning = Color(hex: 0xF59E0B)
static let darkDanger = Color(hex: 0xFF6E6E)
// Light
static let lightBgCanvas = Color(hex: 0xF6F8FC)
static let lightBgElevated = Color(hex: 0xEEF2FA)
static let lightSurfaceCard = Color.white
static let lightSurfaceMuted = Color(hex: 0xF3F5FA)
static let lightTextPrimary = Color(hex: 0x0E1320)
static let lightTextSecondary = Color(hex: 0x55637A)
static let lightTextTertiary = Color(hex: 0x6C7C98)
static let lightAccentPrimary = Color(hex: 0x5A8CFF)
static let lightAccentSecondary = Color(hex: 0x2EE6D6)
static let lightSuccess = Color(hex: 0x13956A)
static let lightWarning = Color(hex: 0xB87504)
static let lightDanger = Color(hex: 0xD24242)
// Brain Gradients
static let brainWork = Gradient(colors: [Color(hex: 0x5A8CFF), Color(hex: 0x2EE6D6)])
static let brainHome = Gradient(colors: [Color(hex: 0xFF6E6E), Color(hex: 0xFFD166)])
static let brainMoney = Gradient(colors: [Color(hex: 0x34D399), Color(hex: 0x2EE6D6)])
static let brainHealth = Gradient(colors: [Color(hex: 0x2EE6D6), Color(hex: 0x9FE870)])
static let brainGlobal = Gradient(colors: [Color(hex: 0x7D8FB4), Color(hex: 0xA5B1C7)])
}
struct MindLystSpacing {
static let x0: CGFloat = 0
static let x1: CGFloat = 4
static let x2: CGFloat = 8
static let x3: CGFloat = 12
static let x4: CGFloat = 16
static let x5: CGFloat = 20
static let x6: CGFloat = 24
static let x7: CGFloat = 28
static let x8: CGFloat = 32
static let x10: CGFloat = 40
static let x12: CGFloat = 48
static let x16: CGFloat = 64
}
struct MindLystRadius {
static let xs: CGFloat = 8
static let sm: CGFloat = 12
static let md: CGFloat = 16
static let lg: CGFloat = 20
static let xl: CGFloat = 24
static let pill: CGFloat = 999
}
struct MindLystMotion {
static let instant: Double = 0.07
static let fast: Double = 0.14
static let base: Double = 0.22
static let slow: Double = 0.32
}
// MARK: - Color Hex Extension
extension Color {
init(hex: UInt, alpha: Double = 1.0) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xFF) / 255.0,
green: Double((hex >> 8) & 0xFF) / 255.0,
blue: Double(hex & 0xFF) / 255.0,
opacity: alpha
)
}
}

View File

@ -0,0 +1,129 @@
// 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 {
// ── Color Palette ────────────────────────────────────────────────
object Palette {
const val NEUTRAL_0 = 0xFFFFFFFF
const val NEUTRAL_50 = 0xFFF6F8FC
const val NEUTRAL_100 = 0xFFEEF2FA
const val NEUTRAL_200 = 0xFFDCE4F2
const val NEUTRAL_300 = 0xFFBFCBDE
const val NEUTRAL_400 = 0xFF92A1BA
const val NEUTRAL_500 = 0xFF6C7C98
const val NEUTRAL_600 = 0xFF55637A
const val NEUTRAL_700 = 0xFF3B455A
const val NEUTRAL_800 = 0xFF1A2335
const val NEUTRAL_900 = 0xFF0E1320
const val NEUTRAL_950 = 0xFF06070A
const val BLUE = 0xFF5A8CFF
const val CYAN = 0xFF2EE6D6
const val CORAL = 0xFFFF6E6E
const val GOLD = 0xFFFFD166
const val MINT = 0xFF34D399
const val WARNING = 0xFFF59E0B
}
// ── Semantic Colors (Dark Theme) ─────────────────────────────────
object Dark {
const val BG_CANVAS = 0xFF06070A
const val BG_ELEVATED = 0xFF0E1118
const val SURFACE_CARD = 0xFF121725
const val SURFACE_MUTED = 0xFF1A2335
const val TEXT_PRIMARY = 0xFFEFF4FF
const val TEXT_SECONDARY = 0xFFA5B1C7
const val TEXT_TERTIARY = 0xFF6C7C98
const val ACCENT_PRIMARY = 0xFF5A8CFF
const val ACCENT_SECONDARY = 0xFF2EE6D6
const val SUCCESS = 0xFF34D399
const val WARNING = 0xFFF59E0B
const val DANGER = 0xFFFF6E6E
}
// ── Semantic Colors (Light Theme) ────────────────────────────────
object Light {
const val BG_CANVAS = 0xFFF6F8FC
const val BG_ELEVATED = 0xFFEEF2FA
const val SURFACE_CARD = 0xFFFFFFFF
const val SURFACE_MUTED = 0xFFF3F5FA
const val TEXT_PRIMARY = 0xFF0E1320
const val TEXT_SECONDARY = 0xFF55637A
const val TEXT_TERTIARY = 0xFF6C7C98
const val ACCENT_PRIMARY = 0xFF5A8CFF
const val ACCENT_SECONDARY = 0xFF2EE6D6
const val SUCCESS = 0xFF13956A
const val WARNING = 0xFFB87504
const val DANGER = 0xFFD24242
}
// ── Brain Identity Gradients ─────────────────────────────────────
data class BrainGradient(val from: Long, val to: Long)
val BRAIN_WORK = BrainGradient(from = 0xFF5A8CFF, to = 0xFF2EE6D6)
val BRAIN_HOME = BrainGradient(from = 0xFFFF6E6E, to = 0xFFFFD166)
val BRAIN_MONEY = BrainGradient(from = 0xFF34D399, to = 0xFF2EE6D6)
val BRAIN_HEALTH = BrainGradient(from = 0xFF2EE6D6, to = 0xFF9FE870)
val BRAIN_GLOBAL = BrainGradient(from = 0xFF7D8FB4, to = 0xFFA5B1C7)
// ── Spacing (8pt grid) ───────────────────────────────────────────
object Spacing {
const val X0 = 0
const val X1 = 4
const val X2 = 8
const val X3 = 12
const val X4 = 16
const val X5 = 20
const val X6 = 24
const val X7 = 28
const val X8 = 32
const val X10 = 40
const val X12 = 48
const val X16 = 64
}
// ── Radius ───────────────────────────────────────────────────────
object Radius {
const val XS = 8
const val SM = 12
const val MD = 16
const val LG = 20
const val XL = 24
const val PILL = 999
}
// ── Typography ───────────────────────────────────────────────────
object Typography {
const val FONT_DISPLAY = "Space Grotesk"
const val FONT_BODY = "DM Sans"
const val FONT_MONO = "IBM Plex Mono"
const val SIZE_XS = 12
const val SIZE_SM = 14
const val SIZE_MD = 16
const val SIZE_LG = 18
const val SIZE_XL = 22
const val SIZE_2XL = 28
const val SIZE_3XL = 36
}
// ── Motion ───────────────────────────────────────────────────────
object Motion {
const val INSTANT = 70
const val FAST = 140
const val BASE = 220
const val SLOW = 320
}
// ── Layout ───────────────────────────────────────────────────────
object Layout {
const val TOUCH_TARGET_MIN = 44
const val MOBILE_GUTTER = 16
const val MAX_WIDTH = 1280
}
}

View File

@ -0,0 +1,78 @@
/* Auto-generated from bytelyst.tokens.json — do not edit manually */
:root,
[data-theme="dark"] {
--ml-bg-canvas: #06070A;
--ml-bg-elevated: #0E1118;
--ml-surface-card: #121725;
--ml-surface-muted: #1A2335;
--ml-border-default: rgba(255,255,255,0.12);
--ml-border-strong: rgba(255,255,255,0.22);
--ml-text-primary: #EFF4FF;
--ml-text-secondary: #A5B1C7;
--ml-text-tertiary: #6C7C98;
--ml-accent-primary: #5A8CFF;
--ml-accent-secondary: #2EE6D6;
--ml-success: #34D399;
--ml-warning: #F59E0B;
--ml-danger: #FF6E6E;
--ml-focus-ring: rgba(90,140,255,0.45);
--ml-overlay-scrim: rgba(5,8,18,0.72);
--ml-font-display: "Space Grotesk", "SF Pro Display", sans-serif;
--ml-font-body: "DM Sans", "SF Pro Text", sans-serif;
--ml-font-mono: "IBM Plex Mono", "SF Mono", monospace;
--ml-fs-xs: 12px;
--ml-fs-sm: 14px;
--ml-fs-md: 16px;
--ml-fs-lg: 18px;
--ml-fs-xl: 22px;
--ml-fs-2xl: 28px;
--ml-fs-3xl: 36px;
--ml-space-0: 0;
--ml-space-1: 4px;
--ml-space-2: 8px;
--ml-space-3: 12px;
--ml-space-4: 16px;
--ml-space-5: 20px;
--ml-space-6: 24px;
--ml-space-7: 28px;
--ml-space-8: 32px;
--ml-space-10: 40px;
--ml-space-12: 48px;
--ml-space-16: 64px;
--ml-radius-xs: 8px;
--ml-radius-sm: 12px;
--ml-radius-md: 16px;
--ml-radius-lg: 20px;
--ml-radius-xl: 24px;
--ml-radius-pill: 999px;
--ml-elevation-sm: 0 4px 12px rgba(0,0,0,0.12);
--ml-elevation-md: 0 12px 28px rgba(0,0,0,0.18);
--ml-elevation-lg: 0 20px 48px rgba(0,0,0,0.24);
--ml-motion-fast: 140ms;
--ml-motion-base: 220ms;
--ml-motion-slow: 320ms;
--ml-easing-standard: cubic-bezier(0.2, 0.0, 0.2, 1);
}
[data-theme="light"] {
--ml-bg-canvas: #F6F8FC;
--ml-bg-elevated: #EEF2FA;
--ml-surface-card: #FFFFFF;
--ml-surface-muted: #F3F5FA;
--ml-border-default: rgba(14,19,32,0.12);
--ml-border-strong: rgba(14,19,32,0.24);
--ml-text-primary: #0E1320;
--ml-text-secondary: #55637A;
--ml-success: #13956A;
--ml-warning: #B87504;
--ml-danger: #D24242;
--ml-focus-ring: rgba(90,140,255,0.35);
--ml-overlay-scrim: rgba(10,13,23,0.5);
}

View File

@ -0,0 +1,184 @@
// Auto-generated from bytelyst.tokens.json — do not edit manually
export const tokens = {
"meta": {
"name": "ByteLyst Design Tokens",
"version": "1.0.0",
"updatedAt": "2026-02-12",
"scale": "8pt"
},
"color": {
"palette": {
"neutral": {
"0": "#FFFFFF",
"50": "#F6F8FC",
"100": "#EEF2FA",
"200": "#DCE4F2",
"300": "#BFCBDE",
"400": "#92A1BA",
"500": "#6C7C98",
"600": "#55637A",
"700": "#3B455A",
"800": "#1A2335",
"900": "#0E1320",
"950": "#06070A"
},
"brand": {
"blue": "#5A8CFF",
"cyan": "#2EE6D6",
"coral": "#FF6E6E",
"gold": "#FFD166",
"mint": "#34D399",
"warning": "#F59E0B"
}
},
"semantic": {
"dark": {
"bgCanvas": "#06070A",
"bgElevated": "#0E1118",
"surfaceCard": "#121725",
"surfaceMuted": "#1A2335",
"borderDefault": "rgba(255,255,255,0.12)",
"borderStrong": "rgba(255,255,255,0.22)",
"textPrimary": "#EFF4FF",
"textSecondary": "#A5B1C7",
"textTertiary": "#6C7C98",
"accentPrimary": "#5A8CFF",
"accentSecondary": "#2EE6D6",
"success": "#34D399",
"warning": "#F59E0B",
"danger": "#FF6E6E",
"focusRing": "rgba(90,140,255,0.45)",
"overlayScrim": "rgba(5,8,18,0.72)"
},
"light": {
"bgCanvas": "#F6F8FC",
"bgElevated": "#EEF2FA",
"surfaceCard": "#FFFFFF",
"surfaceMuted": "#F3F5FA",
"borderDefault": "rgba(14,19,32,0.12)",
"borderStrong": "rgba(14,19,32,0.24)",
"textPrimary": "#0E1320",
"textSecondary": "#55637A",
"textTertiary": "#6C7C98",
"accentPrimary": "#5A8CFF",
"accentSecondary": "#2EE6D6",
"success": "#13956A",
"warning": "#B87504",
"danger": "#D24242",
"focusRing": "rgba(90,140,255,0.35)",
"overlayScrim": "rgba(10,13,23,0.5)"
}
},
"brain": {
"work": {
"from": "#5A8CFF",
"to": "#2EE6D6"
},
"home": {
"from": "#FF6E6E",
"to": "#FFD166"
},
"money": {
"from": "#34D399",
"to": "#2EE6D6"
},
"health": {
"from": "#2EE6D6",
"to": "#9FE870"
},
"global": {
"from": "#7D8FB4",
"to": "#A5B1C7"
}
}
},
"typography": {
"fontFamily": {
"display": "'Space Grotesk', 'SF Pro Display', sans-serif",
"body": "'DM Sans', 'SF Pro Text', sans-serif",
"mono": "'IBM Plex Mono', 'SF Mono', monospace"
},
"fontWeight": {
"regular": 400,
"medium": 500,
"semibold": 600,
"bold": 700
},
"fontSize": {
"xs": 12,
"sm": 14,
"md": 16,
"lg": 18,
"xl": 22,
"2xl": 28,
"3xl": 36
},
"lineHeight": {
"tight": 1.2,
"normal": 1.45,
"relaxed": 1.65
},
"letterSpacing": {
"tight": -0.02,
"normal": 0,
"wide": 0.02
}
},
"spacing": {
"0": 0,
"1": 4,
"2": 8,
"3": 12,
"4": 16,
"5": 20,
"6": 24,
"7": 28,
"8": 32,
"10": 40,
"12": 48,
"16": 64
},
"radius": {
"xs": 8,
"sm": 12,
"md": 16,
"lg": 20,
"xl": 24,
"pill": 999
},
"elevation": {
"none": "0 0 0 rgba(0,0,0,0)",
"sm": "0 4px 12px rgba(0,0,0,0.12)",
"md": "0 12px 28px rgba(0,0,0,0.18)",
"lg": "0 20px 48px rgba(0,0,0,0.24)"
},
"motion": {
"duration": {
"instant": 70,
"fast": 140,
"base": 220,
"slow": 320
},
"easing": {
"standard": "cubic-bezier(0.2, 0.0, 0.2, 1)",
"decelerate": "cubic-bezier(0.0, 0.0, 0.2, 1)",
"accelerate": "cubic-bezier(0.4, 0.0, 1, 1)"
}
},
"breakpoints": {
"mobile": 0,
"tablet": 768,
"desktop": 1200,
"wide": 1440
},
"layout": {
"maxContentWidth": 1280,
"mobileGutter": 16,
"tabletGutter": 24,
"desktopGutter": 32,
"touchTargetMin": 44
}
} as const;
export type Tokens = typeof tokens;

View File

@ -5,6 +5,11 @@
* 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
*/
@ -20,43 +25,92 @@ 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 {",
":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)) {
lines.push(` --ml-font-${key}: ${value};`);
// 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-text-${key}: ${value}px;`);
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}px;`);
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
// Elevation (--ml-elevation-* to match existing)
for (const [key, value] of Object.entries(tokens.elevation)) {
lines.push(` --ml-shadow-${key}: ${value};`);
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");
}
@ -78,38 +132,103 @@ function generateKotlin(): 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 {",
"",
" // Semantic colors (dark)",
" object Colors {",
];
// ── 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("#")) {
const hex = value.replace("#", "");
lines.push(` const val ${key} = 0xFF${hex.toUpperCase()}`);
lines.push(` const val ${camelToScreamingSnake(key)} = 0xFF${value.replace("#", "").toUpperCase()}`);
}
}
lines.push(" }", "");
lines.push(" }", "", " // Brain gradients", " object Brains {");
// ── 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 }][]) {
const fromHex = grad.from.replace("#", "");
const toHex = grad.to.replace("#", "");
lines.push(` val ${name}From = 0xFF${fromHex.toUpperCase()}`);
lines.push(` val ${name}To = 0xFF${toHex.toUpperCase()}`);
lines.push(` val BRAIN_${name.toUpperCase()} = BrainGradient(from = 0xFF${grad.from.replace("#", "").toUpperCase()}, to = 0xFF${grad.to.replace("#", "").toUpperCase()})`);
}
lines.push("");
lines.push(" }", "", " // Spacing (8pt grid)", " object Spacing {");
// ── 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(` const val X${key} = ${value}`);
}
lines.push(" }", "");
lines.push(" }", "", " // Radius", " object Radius {");
// ── Radius
lines.push(" // ── Radius ───────────────────────────────────────────────────────");
lines.push(" object Radius {");
for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` const val ${key} = ${value}`);
lines.push(` const val ${key.toUpperCase()} = ${value}`);
}
lines.push(" }", "");
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");
}
@ -119,34 +238,89 @@ function generateSwift(): 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 ${key} = Color(hex: "${value}")`);
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("");
lines.push("}", "", "struct MindLystSpacing {");
// 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)) {
lines.push(` static let x${key}: CGFloat = ${value}`);
const pad = key.length === 1 ? " " : "";
lines.push(` static let x${key}: ${pad}CGFloat = ${value}`);
}
lines.push("}", "");
lines.push("}", "", "struct MindLystRadius {");
// Radius
lines.push("struct MindLystRadius {");
for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` static let ${key}: CGFloat = ${value}`);
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("}", "");
lines.push("}");
return lines.join("\n");
}
// ── Helpers ──────────────────────────────────────────────────────────
function camelToKebab(str: string): string {
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
// ── Write all ────────────────────────────────────────────────────────
writeFileSync(resolve(outDir, "tokens.css"), generateCSS());
writeFileSync(resolve(outDir, "tokens.ts"), generateTS());