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:
parent
4ae7a9d023
commit
09c767295a
88
packages/design-tokens/generated/MindLystTheme.swift
Normal file
88
packages/design-tokens/generated/MindLystTheme.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
129
packages/design-tokens/generated/MindLystTokens.kt
Normal file
129
packages/design-tokens/generated/MindLystTokens.kt
Normal 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
|
||||
}
|
||||
}
|
||||
78
packages/design-tokens/generated/tokens.css
Normal file
78
packages/design-tokens/generated/tokens.css
Normal 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);
|
||||
}
|
||||
184
packages/design-tokens/generated/tokens.ts
Normal file
184
packages/design-tokens/generated/tokens.ts
Normal 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;
|
||||
@ -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());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user