clear old assets, new ci/cd flow
This commit is contained in:
334
scripts/generate-tokens.mjs
Normal file
334
scripts/generate-tokens.mjs
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate-tokens.mjs
|
||||
*
|
||||
* Reads design-tokens/*.json and generates platform-specific code:
|
||||
* - web/src/theme/tokens.ts
|
||||
* - iOS/Kordant/Theme/GeneratedTokens.swift
|
||||
* - android/app/src/main/res/values/generated_tokens.xml
|
||||
*
|
||||
* Usage: node scripts/generate-tokens.mjs
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, "..");
|
||||
const tokensDir = join(root, "design-tokens");
|
||||
|
||||
function load(name) {
|
||||
return JSON.parse(readFileSync(join(tokensDir, `${name}.json`), "utf-8"));
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function toCamel(key) {
|
||||
return key.replace(/([A-Z])/g, (_, c) => `_${c}`).toLowerCase();
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const h = hex.replace("#", "");
|
||||
return {
|
||||
r: parseInt(h.substring(0, 2), 16),
|
||||
g: parseInt(h.substring(2, 4), 16),
|
||||
b: parseInt(h.substring(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Web (TypeScript) ──────────────────────────────────────────────────────
|
||||
|
||||
function generateWebTokens(colors, typography, spacing, shadows, radius) {
|
||||
const lines = [
|
||||
"// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY",
|
||||
"// Run: node scripts/generate-tokens.mjs",
|
||||
"",
|
||||
];
|
||||
|
||||
// Colors
|
||||
lines.push("export const tokenColors = {");
|
||||
|
||||
// Brand
|
||||
lines.push(" brand: {");
|
||||
for (const [key, val] of Object.entries(colors.brand)) {
|
||||
lines.push(` ${toCamel(key)}: "${val.value}",`);
|
||||
}
|
||||
lines.push(" },");
|
||||
|
||||
// Semantic
|
||||
lines.push(" semantic: {");
|
||||
for (const [key, val] of Object.entries(colors.semantic)) {
|
||||
if (typeof val === "string") {
|
||||
lines.push(` ${toCamel(key)}: "${val}",`);
|
||||
} else if (val.light !== undefined) {
|
||||
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
|
||||
}
|
||||
}
|
||||
lines.push(" },");
|
||||
|
||||
// Background
|
||||
lines.push(" background: {");
|
||||
for (const [key, val] of Object.entries(colors.background)) {
|
||||
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
|
||||
}
|
||||
lines.push(" },");
|
||||
|
||||
// Text
|
||||
lines.push(" text: {");
|
||||
for (const [key, val] of Object.entries(colors.text)) {
|
||||
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
|
||||
}
|
||||
lines.push(" },");
|
||||
|
||||
// Border
|
||||
lines.push(" border: {");
|
||||
for (const [key, val] of Object.entries(colors.border)) {
|
||||
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
|
||||
}
|
||||
lines.push(" },");
|
||||
|
||||
lines.push("};");
|
||||
|
||||
// Typography
|
||||
lines.push("");
|
||||
lines.push("export const tokenTypography = {");
|
||||
lines.push(` fontFamily: "${typography.fontFamily.value}",`);
|
||||
lines.push(` fallback: "${typography.fontFamily.fallback}",`);
|
||||
lines.push(" scale: {");
|
||||
for (const [key, val] of Object.entries(typography.scale)) {
|
||||
lines.push(` ${toCamel(key)}: { size: "${val.size}", lineHeight: "${val.lineHeight}" },`);
|
||||
}
|
||||
lines.push(" },");
|
||||
lines.push(" weights: {");
|
||||
for (const [key, val] of Object.entries(typography.weights)) {
|
||||
lines.push(` ${key}: ${val.value},`);
|
||||
}
|
||||
lines.push(" },");
|
||||
lines.push("};");
|
||||
|
||||
// Spacing
|
||||
lines.push("");
|
||||
lines.push("export const tokenSpacing = {");
|
||||
for (const [key, val] of Object.entries(spacing.scale)) {
|
||||
lines.push(` ${key}: "${val.value}",`);
|
||||
}
|
||||
lines.push("};");
|
||||
|
||||
// Shadows
|
||||
lines.push("");
|
||||
lines.push("export const tokenShadows = {");
|
||||
for (const [key, val] of Object.entries(shadows.scale)) {
|
||||
const shadow = `${val.x}px ${val.y}px ${val.blur}px ${val.spread}px ${val.color}`;
|
||||
lines.push(` ${key}: "${shadow}",`);
|
||||
}
|
||||
lines.push("};");
|
||||
|
||||
// Radius
|
||||
lines.push("");
|
||||
lines.push("export const tokenRadius = {");
|
||||
for (const [key, val] of Object.entries(radius.scale)) {
|
||||
lines.push(` ${key}: "${val.value}",`);
|
||||
}
|
||||
lines.push("};");
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// ─── iOS (Swift) ────────────────────────────────────────────────────────────
|
||||
|
||||
function generateSwiftTokens(colors, typography, spacing) {
|
||||
const lines = [
|
||||
"// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY",
|
||||
"// Run: node scripts/generate-tokens.mjs",
|
||||
"",
|
||||
"import SwiftUI",
|
||||
"",
|
||||
];
|
||||
|
||||
// Color extension
|
||||
lines.push("extension Color {");
|
||||
lines.push(" // MARK: - Brand");
|
||||
for (const [key, val] of Object.entries(colors.brand)) {
|
||||
const { r, g, b } = hexToRgb(val.value);
|
||||
const swiftKey = key.charAt(0).toLowerCase() + key.slice(1);
|
||||
lines.push(
|
||||
` static let ${swiftKey} = Color(red: ${r} / 255, green: ${g} / 255, blue: ${b} / 255)`
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(" // MARK: - Semantic");
|
||||
for (const [key, val] of Object.entries(colors.semantic)) {
|
||||
if (val.value) {
|
||||
const { r, g, b } = hexToRgb(val.value);
|
||||
const swiftKey = key.charAt(0).toLowerCase() + key.slice(1);
|
||||
lines.push(
|
||||
` static let ${swiftKey} = Color(red: ${r} / 255, green: ${g} / 255, blue: ${b} / 255)`
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push("}");
|
||||
|
||||
// AdaptiveColor helper
|
||||
lines.push("");
|
||||
lines.push("extension UIColor {");
|
||||
lines.push(" convenience init(hex: String) {");
|
||||
lines.push(
|
||||
' var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)'
|
||||
);
|
||||
lines.push(
|
||||
' hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")'
|
||||
);
|
||||
lines.push(" var rgb: UInt64 = 0");
|
||||
lines.push(" Scanner(string: hexSanitized).scanHexInt64(&rgb)");
|
||||
lines.push(" let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0");
|
||||
lines.push(" let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0");
|
||||
lines.push(" let b = CGFloat(rgb & 0x0000FF) / 255.0");
|
||||
lines.push(" self.init(red: r, green: g, blue: b, alpha: 1.0)");
|
||||
lines.push(" }");
|
||||
lines.push("}");
|
||||
|
||||
// Spacing
|
||||
lines.push("");
|
||||
lines.push("enum DesignTokens {");
|
||||
lines.push(" enum Spacing {");
|
||||
for (const [key, val] of Object.entries(spacing.scale)) {
|
||||
if (key === "0") continue; // skip numeric key — invalid in Swift
|
||||
const px = parseInt(val.value);
|
||||
lines.push(` static let ${key}: CGFloat = ${px}`);
|
||||
}
|
||||
lines.push(" }");
|
||||
lines.push("}");
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// ─── Android (XML) ──────────────────────────────────────────────────────────
|
||||
|
||||
function generateAndroidTokens(colors, typography, spacing, shadows, radius) {
|
||||
const lines = [
|
||||
'<?xml version="1.0" encoding="utf-8"?>',
|
||||
"<!-- Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY -->",
|
||||
"<!-- Run: node scripts/generate-tokens.mjs -->",
|
||||
"<resources>",
|
||||
];
|
||||
|
||||
// Brand colors
|
||||
lines.push(" <!-- Brand -->");
|
||||
for (const [key, val] of Object.entries(colors.brand)) {
|
||||
const name = "brand_" + toCamel(key);
|
||||
lines.push(` <color name="${name}">${val.value}</color>`);
|
||||
}
|
||||
|
||||
// Semantic colors
|
||||
lines.push(" <!-- Semantic -->");
|
||||
for (const [key, val] of Object.entries(colors.semantic)) {
|
||||
if (typeof val === "string") {
|
||||
const name = "sem_" + toCamel(key);
|
||||
lines.push(` <color name="${name}">${val}</color>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Light theme colors
|
||||
lines.push(" <!-- Light theme -->");
|
||||
lines.push(
|
||||
` <color name="bg_light">${colors.background.bg.light}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="bg_secondary_light">${colors.background.bgSecondary.light}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="text_primary_light">${colors.text.textPrimary.light}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="text_secondary_light">${colors.text.textSecondary.light}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="border_light">${colors.border.border.light}</color>`
|
||||
);
|
||||
|
||||
// Dark theme colors
|
||||
lines.push(" <!-- Dark theme -->");
|
||||
lines.push(
|
||||
` <color name="bg_dark">${colors.background.bg.dark}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="bg_secondary_dark">${colors.background.bgSecondary.dark}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="text_primary_dark">${colors.text.textPrimary.dark}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="text_secondary_dark">${colors.text.textSecondary.dark}</color>`
|
||||
);
|
||||
lines.push(
|
||||
` <color name="border_dark">${colors.border.border.dark}</color>`
|
||||
);
|
||||
|
||||
// Spacing dimensions
|
||||
lines.push(" <!-- Spacing -->");
|
||||
for (const [key, val] of Object.entries(spacing.scale)) {
|
||||
const dp = val.value.replace("px", "dp");
|
||||
lines.push(` <dimen name="spacing_${key}">${dp}</dimen>`);
|
||||
}
|
||||
|
||||
// Border radius
|
||||
lines.push(" <!-- Corner radius -->");
|
||||
for (const [key, val] of Object.entries(radius.scale)) {
|
||||
const name = key === "full" ? "corner_full" : `corner_${key}`;
|
||||
const dp = val.value.replace("px", "dp");
|
||||
lines.push(` <dimen name="${name}">${dp}</dimen>`);
|
||||
}
|
||||
|
||||
// Font sizes
|
||||
lines.push(" <!-- Typography -->");
|
||||
for (const [key, val] of Object.entries(typography.scale)) {
|
||||
lines.push(` <dimen name="font_${toCamel(key)}">${val.size.replace("px", "sp")}</dimen>`);
|
||||
lines.push(` <dimen name="font_${toCamel(key)}_lh">${val.lineHeight.replace("px", "sp")}</dimen>`);
|
||||
}
|
||||
|
||||
lines.push("</resources>");
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function main() {
|
||||
const colors = load("colors");
|
||||
const typography = load("typography");
|
||||
const spacing = load("spacing");
|
||||
const shadows = load("shadows");
|
||||
const radius = load("radius");
|
||||
|
||||
// Web
|
||||
const webPath = join(root, "web", "src", "theme", "tokens.ts");
|
||||
mkdirSync(dirname(webPath), { recursive: true });
|
||||
writeFileSync(webPath, generateWebTokens(colors, typography, spacing, shadows, radius));
|
||||
console.log(`✅ ${webPath}`);
|
||||
|
||||
// iOS
|
||||
const iosPath = join(root, "iOS", "Kordant", "Theme", "GeneratedTokens.swift");
|
||||
mkdirSync(dirname(iosPath), { recursive: true });
|
||||
writeFileSync(iosPath, generateSwiftTokens(colors, typography, spacing));
|
||||
console.log(`✅ ${iosPath}`);
|
||||
|
||||
// Android
|
||||
const androidPath = join(
|
||||
root,
|
||||
"android",
|
||||
"app",
|
||||
"src",
|
||||
"main",
|
||||
"res",
|
||||
"values",
|
||||
"generated_tokens.xml"
|
||||
);
|
||||
mkdirSync(dirname(androidPath), { recursive: true });
|
||||
writeFileSync(androidPath, generateAndroidTokens(colors, typography, spacing, shadows, radius));
|
||||
console.log(`✅ ${androidPath}`);
|
||||
|
||||
console.log("🎨 Token generation complete.");
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user