From b80d249c78b3666c5a38a98aec3e5438b86d5167 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 12 Feb 2026 11:22:52 -0800 Subject: [PATCH] feat(design-tokens): add @bytelyst/design-tokens package - Canonical bytelyst.tokens.json with colors, typography, spacing, radius, elevation, motion, breakpoints - loadTokens() for programmatic access - generate.ts script outputs 4 platform formats: - tokens.css (CSS custom properties with --ml-* prefix) - tokens.ts (TypeScript constants) - MindLystTokens.kt (Kotlin object for KMP shared module) - MindLystTheme.swift (Swift structs for SwiftUI) - Shared across LysnrAI dashboards and MindLyst native apps --- packages/design-tokens/package.json | 24 +++ packages/design-tokens/scripts/generate.ts | 156 +++++++++++++++++ packages/design-tokens/src/index.ts | 49 ++++++ .../design-tokens/tokens/bytelyst.tokens.json | 165 ++++++++++++++++++ packages/design-tokens/tsconfig.json | 9 + 5 files changed, 403 insertions(+) create mode 100644 packages/design-tokens/package.json create mode 100644 packages/design-tokens/scripts/generate.ts create mode 100644 packages/design-tokens/src/index.ts create mode 100644 packages/design-tokens/tokens/bytelyst.tokens.json create mode 100644 packages/design-tokens/tsconfig.json diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json new file mode 100644 index 00000000..716b6c28 --- /dev/null +++ b/packages/design-tokens/package.json @@ -0,0 +1,24 @@ +{ + "name": "@bytelyst/design-tokens", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./tokens.json": "./tokens/bytelyst.tokens.json", + "./css": "./generated/tokens.css" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist", "tokens", "generated", "scripts"], + "scripts": { + "build": "tsc", + "generate": "tsx scripts/generate.ts", + "test": "vitest run" + }, + "devDependencies": { + "tsx": "^4.0.0" + } +} diff --git a/packages/design-tokens/scripts/generate.ts b/packages/design-tokens/scripts/generate.ts new file mode 100644 index 00000000..83d7a41e --- /dev/null +++ b/packages/design-tokens/scripts/generate.ts @@ -0,0 +1,156 @@ +/** + * 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 + * + * Usage: tsx scripts/generate.ts + */ + +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const tokensPath = resolve(__dirname, "../tokens/bytelyst.tokens.json"); +const outDir = resolve(__dirname, "../generated"); + +mkdirSync(outDir, { recursive: true }); + +const tokens = JSON.parse(readFileSync(tokensPath, "utf-8")); + +// ── 1. CSS ─────────────────────────────────────────────────────────── +function generateCSS(): string { + const lines: string[] = [ + "/* Auto-generated from bytelyst.tokens.json — do not edit manually */", + "", + ":root {", + ]; + + // Semantic colors (dark theme as default) + for (const [key, value] of Object.entries(tokens.color.semantic.dark)) { + lines.push(` --ml-${camelToKebab(key)}: ${value};`); + } + + // Typography + for (const [key, value] of Object.entries(tokens.typography.fontFamily)) { + lines.push(` --ml-font-${key}: ${value};`); + } + for (const [key, value] of Object.entries(tokens.typography.fontSize)) { + lines.push(` --ml-text-${key}: ${value}px;`); + } + + // Spacing + for (const [key, value] of Object.entries(tokens.spacing)) { + lines.push(` --ml-space-${key}: ${value}px;`); + } + + // Radius + for (const [key, value] of Object.entries(tokens.radius)) { + lines.push(` --ml-radius-${key}: ${value}px;`); + } + + // Elevation + for (const [key, value] of Object.entries(tokens.elevation)) { + lines.push(` --ml-shadow-${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", + "", + "object MindLystTokens {", + "", + " // Semantic colors (dark)", + " object Colors {", + ]; + + 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(" }", "", " // Brain gradients", " object Brains {"); + 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(" }", "", " // Spacing (8pt grid)", " object Spacing {"); + for (const [key, value] of Object.entries(tokens.spacing)) { + lines.push(` const val x${key} = ${value}`); + } + + lines.push(" }", "", " // Radius", " object Radius {"); + for (const [key, value] of Object.entries(tokens.radius)) { + lines.push(` const val ${key} = ${value}`); + } + + 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", + "", + "struct MindLystColors {", + ]; + + 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("}", "", "struct MindLystSpacing {"); + for (const [key, value] of Object.entries(tokens.spacing)) { + lines.push(` static let x${key}: CGFloat = ${value}`); + } + + lines.push("}", "", "struct MindLystRadius {"); + for (const [key, value] of Object.entries(tokens.radius)) { + lines.push(` static let ${key}: CGFloat = ${value}`); + } + + 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()); +writeFileSync(resolve(outDir, "MindLystTokens.kt"), generateKotlin()); +writeFileSync(resolve(outDir, "MindLystTheme.swift"), generateSwift()); + +console.log("Generated 4 token files in generated/"); diff --git a/packages/design-tokens/src/index.ts b/packages/design-tokens/src/index.ts new file mode 100644 index 00000000..c0b832c5 --- /dev/null +++ b/packages/design-tokens/src/index.ts @@ -0,0 +1,49 @@ +/** + * Design tokens — programmatic access to the canonical token JSON. + * For generated platform files, see the `generated/` directory. + * For the canonical JSON source, see `tokens/bytelyst.tokens.json`. + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export interface DesignTokens { + meta: { name: string; version: string; updatedAt: string; scale: string }; + color: { + palette: Record>; + semantic: { + dark: Record; + light: Record; + }; + brain: Record; + }; + typography: { + fontFamily: Record; + fontWeight: Record; + fontSize: Record; + lineHeight: Record; + letterSpacing: Record; + }; + spacing: Record; + radius: Record; + elevation: Record; + motion: { + duration: Record; + easing: Record; + }; + breakpoints: Record; + layout: Record; +} + +let _cached: DesignTokens | null = null; + +export function loadTokens(): DesignTokens { + if (_cached) return _cached; + const tokenPath = resolve(__dirname, "../tokens/bytelyst.tokens.json"); + const raw = readFileSync(tokenPath, "utf-8"); + _cached = JSON.parse(raw) as DesignTokens; + return _cached; +} diff --git a/packages/design-tokens/tokens/bytelyst.tokens.json b/packages/design-tokens/tokens/bytelyst.tokens.json new file mode 100644 index 00000000..1139563b --- /dev/null +++ b/packages/design-tokens/tokens/bytelyst.tokens.json @@ -0,0 +1,165 @@ +{ + "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 + } +} diff --git a/packages/design-tokens/tsconfig.json b/packages/design-tokens/tsconfig.json new file mode 100644 index 00000000..5edad813 --- /dev/null +++ b/packages/design-tokens/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +}