Skip to content
SlashClone

Palette export

@slashclone/palette-export

Turn your design tokens into a downloadable color palette: a swatch sheet (SVG), JSON, CSS variables, and a GIMP palette that Krita and Inkscape also open. Handy for briefing designers or image models with your exact colors. Reads one colors file and writes only to an out folder.

SkillContentv0.1.0 (latest)Tested
Activity
New: be the first to try it
  1. 1#!/usr/bin/env node
  2. 2// Exports a color palette as JSON, CSS variables, a GIMP palette (.gpl), and an SVG swatch
  3. 3// sheet. Node built-ins only.
  4. 4// Usage: node scripts/export-palette.mjs --in data/colors.json [--name Brand] [--prefix brand]
  5. 5// [--out out]
  6. 6// Input: { "groups": [{ "name", "note"?, "colors": { name: "#RRGGBB" } }] } or a flat
  7. 7// { name: "#RRGGBB" } map.
  8. 8import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
  9. 9import { join } from "node:path";
  10. 10import { parseArgs } from "node:util";
  11. 12const { values } = parseArgs({
  12. 13 options: {
  13. 14 in: { type: "string" },
  14. 15 name: { type: "string", default: "Palette" },
  15. 16 prefix: { type: "string" },
  16. 17 out: { type: "string", default: "out" },
  17. 18 },
  18. 19});
  19. 20if (!values.in) {
  20. 21 console.error("Missing --in: a JSON file of colors.");
  21. 22 process.exit(2);
  22. 25const raw = JSON.parse(readFileSync(values.in, "utf8"));
  23. 26const groups = Array.isArray(raw.groups)
  24. 27 ? raw.groups
  25. 28 : [{ name: values.name, note: "", colors: raw }];
  26. 29const HEX = /^#[0-9a-f]{6}$/i;
  27. 30for (const g of groups) {
  28. 31 for (const [key, hex] of Object.entries(g.colors ?? {})) {
  29. 32 if (typeof hex !== "string" || !HEX.test(hex)) {
  30. 33 console.error(`"${key}" in "${g.name}" is not a #RRGGBB color: ${String(hex)}`);
  31. 34 process.exit(1);
  32. 35 }
  33. 36 }
  34. 39const slug = (s) =>
  35. 40 s
  36. 41 .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
  37. 42 .replace(/[^a-z0-9]+/gi, "-")
  38. 43 .replace(/^-|-$/g, "")
  39. 44 .toLowerCase();
  40. 45const prefix = slug(values.prefix ?? values.name);
  41. 46const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  42. 47const all = groups.flatMap((g) => Object.entries(g.colors));
  43. 48mkdirSync(values.out, { recursive: true });
  44. 50writeFileSync(
  45. 51 join(values.out, "palette.json"),
  46. 52 JSON.stringify(
  47. 53 {
  48. 54 name: values.name,
  49. 55 groups: groups.map((g) => ({ name: g.name, note: g.note ?? "", colors: g.colors })),
  50. 56 },
  51. 57 null,
  52. 58 2,
  53. 59 ) + "\n",
  54. 60);
  55. 62writeFileSync(
  56. 63 join(values.out, "palette.css"),
  57. 64 `/* ${values.name} */\n:root {\n${all.map(([k, c]) => ` --${prefix}-${slug(k)}: ${c};`).join("\n")}\n}\n`,
  58. 65);
  59. 67writeFileSync(
  60. 68 join(values.out, "palette.gpl"),
  61. 69 `GIMP Palette\nName: ${values.name}\nColumns: 8\n#\n${all
  62. 70 .map(([k, c]) => {
  63. 71 const n = parseInt(c.slice(1), 16);
  64. 72 const ch = (s) => String((n >> s) & 255).padStart(3);
  65. 73 return `${ch(16)} ${ch(8)} ${ch(0)}\t${k}`;
  66. 74 })
  67. 75 .join("\n")}\n`,
  68. 76);
  69. 78// Swatch sheet: one row per group, the hex on each chip in a color that reads on it.
  70. 79const luminance = (c) => {
  71. 80 const n = parseInt(c.slice(1), 16);
  72. 81 return (0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)) / 255;
  73. 82};
  74. 83const W = 1600;
  75. 84const cols = 8;
  76. 85const sw = 172;
  77. 86const sh = 112;
  78. 87const gap = 16;
  79. 88let y = 96;
  80. 89const parts = [];
  81. 90for (const g of groups) {
  82. 91 parts.push(
  83. 92 `<text x="48" y="${y}" font-size="24" font-weight="700" fill="#222">${esc(g.name)}</text>`,
  84. 93 );
  85. 94 if (g.note) {
  86. 95 parts.push(`<text x="48" y="${y + 26}" font-size="16" fill="#666">${esc(g.note)}</text>`);
  87. 96 }
  88. 97 y += 48;
  89. 98 Object.entries(g.colors).forEach(([k, c], i) => {
  90. 99 const x = 48 + (i % cols) * (sw + gap);
  91. 100 const yy = y + Math.floor(i / cols) * (sh + 48);
  92. 101 const ink = luminance(c) > 0.55 ? "#222" : "#FFF";
  93. 102 parts.push(
  94. 103 `<rect x="${x}" y="${yy}" width="${sw}" height="${sh}" rx="14" fill="${c}" stroke="#0002"/>` +
  95. 104 `<text x="${x + 12}" y="${yy + sh - 14}" font-size="16" font-family="ui-monospace, Menlo, monospace" fill="${ink}">${c.toUpperCase()}</text>` +
  96. 105 `<text x="${x}" y="${yy + sh + 22}" font-size="15" fill="#222">${esc(k)}</text>`,
  97. 106 );
  98. 107 });
  99. 108 y += Math.ceil(Object.keys(g.colors).length / cols) * (sh + 48) + 24;
  100. 110writeFileSync(
  101. 111 join(values.out, "palette.svg"),
  102. 112 `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${y}" viewBox="0 0 ${W} ${y}" font-family="system-ui, sans-serif">` +
  103. 113 `<rect width="100%" height="100%" fill="#FAFAF7"/>` +
  104. 114 `<text x="48" y="56" font-size="34" font-weight="800" fill="#222">${esc(values.name)}</text>` +
  105. 115 parts.join("") +
  106. 116 `</svg>\n`,
  107. 117);
  108. 119console.log(
  109. 120 `${String(all.length)} colors in ${String(groups.length)} groups -> ${values.out}/palette.{json,css,gpl,svg}`,
  110. 121);