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