- Added @eslint/js dependency - Updated eslint.config.js for ESLint 9 compatibility - Added required globals (crypto, localStorage, React, etc.) - Fixed unused imports and variables - Disabled sort-imports temporarily - Formatted all files with Prettier
350 lines
14 KiB
TypeScript
350 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 { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { dirname, resolve } 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/');
|