Skip to content
SlashClone
Made by@brandkit

Brand tokens export

@brandkit/brand-tokens-export

Turn the tokens in your brand.md into code: CSS variables, a Tailwind theme (v3 and v4), and a design-tokens.json for Figma token plugins. One source of truth, regenerated in a second whenever the brand changes.

SkillContentv1.0.0 (latest)Tested
Activity
New: be the first to try it
  1. 1#!/usr/bin/env node
  2. 2// Turns the tokens block in brand.md into CSS variables, a Tailwind theme, and design-tokens.json.
  3. 3// Usage: node scripts/export-tokens.mjs [brand.md] [out-dir] [--prefix ""]
  4. 4// Writes: <out-dir>/tokens.css, <out-dir>/theme.css (Tailwind v4 @theme),
  5. 5// <out-dir>/tailwind.theme.cjs (Tailwind v3), <out-dir>/design-tokens.json
  6. 6import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
  7. 7import { join } from "node:path";
  8. 8import { flatten, readTokens, slug } from "./tokens-lib.mjs";
  9. 9
  10. 10const args = process.argv.slice(2);
  11. 11function flag(name, fallback) {
  12. 12 const i = args.indexOf(name);
  13. 13 if (i === -1) return fallback;
  14. 14 const value = args[i + 1] ?? "";
  15. 15 args.splice(i, 2);
  16. 16 return value;
  17. 18const prefix = flag("--prefix", "");
  18. 19const [input = "brand.md", outDir = "brand-tokens"] = args;
  19. 21if (!existsSync(input)) {
  20. 22 console.error(`export-tokens: ${input} not found. Pass the path to your brand guide, for example docs/brand.md.`);
  21. 23 process.exit(1);
  22. 26let tokens;
  23. 27try {
  24. 28 tokens = readTokens(readFileSync(input, "utf8"));
  25. 29} catch (err) {
  26. 30 console.error(`export-tokens: ${input}: ${err.message}`);
  27. 31 process.exit(1);
  28. 34// Group in brand.md -> CSS variable namespace (Tailwind v4 names), Tailwind v3 key, DTCG type.
  29. 35const GROUPS = [
  30. 36 { group: "color", css: "color", tw: "colors", type: "color" },
  31. 37 { group: "font", css: "font", tw: "fontFamily", type: "fontFamily" },
  32. 38 { group: "text", css: "text", tw: "fontSize", type: "dimension" },
  33. 39 { group: "weight", css: "font-weight", tw: "fontWeight", type: "fontWeight" },
  34. 40 { group: "space", css: "spacing", tw: "spacing", type: "dimension" },
  35. 41 { group: "radius", css: "radius", tw: "borderRadius", type: "dimension" },
  36. 42 { group: "shadow", css: "shadow", tw: "boxShadow", type: null },
  37. 43];
  38. 45const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
  39. 46const warnings = [];
  40. 47const vars = [];
  41. 48const tailwind = {};
  42. 49const dtcg = {};
  43. 51function fontList(value) {
  44. 52 return String(value)
  45. 53 .split(",")
  46. 54 .map((s) => s.trim())
  47. 55 .filter(Boolean);
  48. 57function cssFont(value) {
  49. 58 return fontList(value)
  50. 59 .map((f) => (/\s/.test(f) && !/^["']/.test(f) ? `"${f}"` : f))
  51. 60 .join(", ");
  52. 63for (const g of GROUPS) {
  53. 64 const entries = flatten(tokens[g.group]);
  54. 65 if (entries.length === 0) continue;
  55. 66 tailwind[g.tw] = {};
  56. 67 dtcg[g.group] = {};
  57. 68 for (const [name, value] of entries) {
  58. 69 const key = slug(name);
  59. 70 if (g.group === "color" && !HEX.test(value) && !/^(rgb|hsl|oklch)a?\(/i.test(value)) {
  60. 71 warnings.push(`color.${name} "${value}" is not a hex, rgb, hsl, or oklch color`);
  61. 72 }
  62. 73 const cssValue = g.group === "font" ? cssFont(value) : value;
  63. 74 vars.push([`--${prefix}${g.css}-${key}`, cssValue]);
  64. 75 tailwind[g.tw][key] = g.group === "font" ? fontList(value) : value;
  65. 76 const token = { $value: g.group === "font" ? fontList(value) : value };
  66. 77 if (g.type) token.$type = g.type;
  67. 78 dtcg[g.group][key] = token;
  68. 79 }
  69. 82if (vars.length === 0) {
  70. 83 console.error(
  71. 84 `export-tokens: ${input}: the tokens block has no color, font, text, weight, space, radius, or shadow groups.`,
  72. 85 );
  73. 86 process.exit(1);
  74. 89const name = tokens.name ? String(tokens.name) : "Brand";
  75. 90const header = `/* ${name} design tokens. Generated from ${input} by brand-tokens-export. Edit brand.md, not this file. */`;
  76. 91const body = vars.map(([k, v]) => ` ${k}: ${v};`).join("\n");
  77. 92const tokensCss = `${header}\n:root {\n${body}\n}\n`;
  78. 93const themeCss = `${header}\n/* Tailwind v4: import after "tailwindcss" in your main CSS file. */\n@theme {\n${body}\n}\n`;
  79. 94const requirePath = outDir.startsWith("/") ? outDir : `./${outDir.replace(/^\.\//, "").replace(/\/$/, "")}`;
  80. 95const twJs = `// ${name} theme for Tailwind v3. Generated from ${input} by brand-tokens-export.\n// In tailwind.config.js: theme: { extend: require("${requirePath}/tailwind.theme.cjs") }\nmodule.exports = ${JSON.stringify(tailwind, null, 2)};\n`;
  81. 96const json = { $description: `${name} design tokens (W3C design tokens format)`, ...dtcg };
  82. 98mkdirSync(outDir, { recursive: true });
  83. 99const files = {
  84. 100 "tokens.css": tokensCss,
  85. 101 "theme.css": themeCss,
  86. 102 "tailwind.theme.cjs": twJs,
  87. 103 "design-tokens.json": `${JSON.stringify(json, null, 2)}\n`,
  88. 104};
  89. 105for (const [file, content] of Object.entries(files)) writeFileSync(join(outDir, file), content);
  90. 107console.log(`Exported ${vars.length} tokens from ${input} to ${outDir}/`);
  91. 108for (const file of Object.keys(files)) console.log(` ${join(outDir, file)}`);
  92. 109for (const w of warnings) console.log(` warning: ${w}`);