diff --git a/packages/design-tokens/generated/MindLystTheme.swift b/packages/design-tokens/generated/MindLystTheme.swift new file mode 100644 index 00000000..0209ca78 --- /dev/null +++ b/packages/design-tokens/generated/MindLystTheme.swift @@ -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 + ) + } +} diff --git a/packages/design-tokens/generated/MindLystTokens.kt b/packages/design-tokens/generated/MindLystTokens.kt new file mode 100644 index 00000000..cbcfc9ba --- /dev/null +++ b/packages/design-tokens/generated/MindLystTokens.kt @@ -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 + } +} diff --git a/packages/design-tokens/generated/tokens.css b/packages/design-tokens/generated/tokens.css new file mode 100644 index 00000000..27d5710b --- /dev/null +++ b/packages/design-tokens/generated/tokens.css @@ -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); +} diff --git a/packages/design-tokens/generated/tokens.ts b/packages/design-tokens/generated/tokens.ts new file mode 100644 index 00000000..ca7d13bb --- /dev/null +++ b/packages/design-tokens/generated/tokens.ts @@ -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; diff --git a/packages/design-tokens/scripts/generate.ts b/packages/design-tokens/scripts/generate.ts index 83d7a41e..f15af71b 100644 --- a/packages/design-tokens/scripts/generate.ts +++ b/packages/design-tokens/scripts/generate.ts @@ -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());