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
This commit is contained in:
parent
cf8781cc11
commit
b80d249c78
24
packages/design-tokens/package.json
Normal file
24
packages/design-tokens/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
156
packages/design-tokens/scripts/generate.ts
Normal file
156
packages/design-tokens/scripts/generate.ts
Normal file
@ -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/");
|
||||||
49
packages/design-tokens/src/index.ts
Normal file
49
packages/design-tokens/src/index.ts
Normal file
@ -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<string, Record<string, string>>;
|
||||||
|
semantic: {
|
||||||
|
dark: Record<string, string>;
|
||||||
|
light: Record<string, string>;
|
||||||
|
};
|
||||||
|
brain: Record<string, { from: string; to: string }>;
|
||||||
|
};
|
||||||
|
typography: {
|
||||||
|
fontFamily: Record<string, string>;
|
||||||
|
fontWeight: Record<string, number>;
|
||||||
|
fontSize: Record<string, number>;
|
||||||
|
lineHeight: Record<string, number>;
|
||||||
|
letterSpacing: Record<string, number>;
|
||||||
|
};
|
||||||
|
spacing: Record<string, number>;
|
||||||
|
radius: Record<string, number>;
|
||||||
|
elevation: Record<string, string>;
|
||||||
|
motion: {
|
||||||
|
duration: Record<string, number>;
|
||||||
|
easing: Record<string, string>;
|
||||||
|
};
|
||||||
|
breakpoints: Record<string, number>;
|
||||||
|
layout: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
165
packages/design-tokens/tokens/bytelyst.tokens.json
Normal file
165
packages/design-tokens/tokens/bytelyst.tokens.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/design-tokens/tsconfig.json
Normal file
9
packages/design-tokens/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.test.ts"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user