@theme-registry/refract 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ var node_url = require('node:url');
9
9
  var node_os = require('node:os');
10
10
  var node_readline = require('node:readline');
11
11
 
12
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
12
13
  /**
13
14
  * Build-layer path + transpile helpers (Node-only).
14
15
  *
@@ -21,8 +22,21 @@ var node_readline = require('node:readline');
21
22
  * and this module resolves them against the discovered package root — working the SAME way in-repo
22
23
  * (root = repo root) and installed (root = the installed package dir).
23
24
  */
25
+ /**
26
+ * This module's own directory, in **both** bundle formats.
27
+ *
28
+ * `__dirname` only exists in the CJS build. The `./build` subpath also ships an ESM bundle, and
29
+ * defaulting to a bare `__dirname` made every ESM consumer of this layer throw `__dirname is not
30
+ * defined` the moment it reached package-root discovery — `runInit`, `runCreate`, `runSkillsInstall`.
31
+ * It went unnoticed because the only consumer was refract's own CLI, which is bundled as CJS.
32
+ *
33
+ * `typeof` guards rather than a feature test, because referencing an undeclared `__dirname` in ESM
34
+ * would be a ReferenceError. Rollup rewrites `import.meta.url` for the CJS output, so the fallback
35
+ * expression is valid in both.
36
+ */
37
+ const moduleDir = () => typeof __dirname !== "undefined" ? __dirname : node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.js', document.baseURI).href))));
24
38
  /** Walk up from `startDir` to the first ancestor that contains a `package.json`. */
25
- const findPackageRoot = (startDir = __dirname) => {
39
+ const findPackageRoot = (startDir = moduleDir()) => {
26
40
  let dir = startDir;
27
41
  for (;;) {
28
42
  if (node_fs.existsSync(node_path.join(dir, "package.json")))
@@ -8004,7 +8018,7 @@ const RATIO_VALUES = {
8004
8018
  * overlap, so a scaffolded literal and an authored `harmony:` key agree. `pentadic` is local — the
8005
8019
  * subsystem has no five-way scheme, and the scaffolder needs one to reach five brand colours.
8006
8020
  */
8007
- const SCHEME_ROTATIONS = {
8021
+ const SCHEME_ROTATIONS$1 = {
8008
8022
  complement: [180],
8009
8023
  analogous: [-30, 30],
8010
8024
  "split-complement": [150, 210],
@@ -8106,7 +8120,7 @@ const defaultSchemeFor = (brandCount) => {
8106
8120
  return "pentadic";
8107
8121
  };
8108
8122
  /** Schemes that produce exactly `brandCount − 1` members — the valid choices at that count. */
8109
- const schemesFor = (brandCount) => Object.keys(SCHEME_ROTATIONS).filter((s) => SCHEME_ROTATIONS[s].length === brandCount - 1);
8123
+ const schemesFor = (brandCount) => Object.keys(SCHEME_ROTATIONS$1).filter((s) => SCHEME_ROTATIONS$1[s].length === brandCount - 1);
8110
8124
  /**
8111
8125
  * The contrast gate. Builds the palette set, audits every text-on-base pairing, and walks the
8112
8126
  * lightness of each failing colour down one OKLCH point at a time until it clears the bar.
@@ -8202,7 +8216,7 @@ function scaffoldTheme(answers) {
8202
8216
  }
8203
8217
  else if (brandCount > 1) {
8204
8218
  const scheme = (_g = answers.scheme) !== null && _g !== void 0 ? _g : defaultSchemeFor(brandCount);
8205
- const rotations = SCHEME_ROTATIONS[scheme];
8219
+ const rotations = SCHEME_ROTATIONS$1[scheme];
8206
8220
  if (!rotations)
8207
8221
  throw new Error(`Unknown harmony scheme "${scheme}".`);
8208
8222
  rotations.slice(0, brandCount - 1).forEach((deg, i) => {
@@ -8460,6 +8474,35 @@ const cursorUp = (n) => (n > 0 ? `\u001b[${n}A` : "");
8460
8474
  const CLEAR_DOWN = "\u001b[0J";
8461
8475
  const HIDE_CURSOR = "\u001b[?25l";
8462
8476
  const SHOW_CURSOR = "\u001b[?25h";
8477
+ /**
8478
+ * Does this terminal do 24-bit colour? `COLORTERM` is the only real signal, and plenty of capable
8479
+ * terminals (Apple Terminal among them) never set it — so a missing value means "approximate", not
8480
+ * "give up". Gating swatches on truecolor alone would silently delete them for those users.
8481
+ */
8482
+ const truecolor = () => { var _a; return /truecolor|24bit/i.test((_a = process.env.COLORTERM) !== null && _a !== void 0 ? _a : ""); };
8483
+ /** Nearest xterm-256 colour-cube index for an RGB triple — the fallback when 24-bit isn't offered. */
8484
+ const to256 = (r, g, b) => {
8485
+ const axis = (v) => (v < 48 ? 0 : v < 114 ? 1 : Math.min(5, Math.round((v - 35) / 40)));
8486
+ return 16 + 36 * axis(r) + 6 * axis(g) + axis(b);
8487
+ };
8488
+ /**
8489
+ * `#rrggbb` → a coloured block: exact in 24-bit terminals, approximated in 256-colour ones, and
8490
+ * empty when there's no colour at all (a pipe, `NO_COLOR`) so redirected output stays clean.
8491
+ *
8492
+ * Approximating is right here — the swatch is a preview, and the exact value is written to the theme
8493
+ * file regardless. An approximately-right hue beats a blank space.
8494
+ */
8495
+ function swatch(hex, width = 2) {
8496
+ if (!useColor())
8497
+ return "";
8498
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
8499
+ if (!m)
8500
+ return "";
8501
+ const n = parseInt(m[1], 16);
8502
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
8503
+ const bg = truecolor() ? `48;2;${r};${g};${b}` : `48;5;${to256(r, g, b)}`;
8504
+ return `\u001b[${bg}m${" ".repeat(width)}\u001b[0m`;
8505
+ }
8463
8506
  /** Decode a raw stdin chunk into a {@link ListKey}. Arrows arrive as escape sequences; j/k mirror vim. */
8464
8507
  function decodeKey(chunk) {
8465
8508
  switch (chunk) {
@@ -8650,13 +8693,24 @@ class Prompter {
8650
8693
  };
8651
8694
  let painted = 0;
8652
8695
  const help = multi ? "↑↓ move · space toggle · a all · enter confirm" : "↑↓ move · enter select";
8696
+ // Pad labels so the hints line up into a column — a ragged right edge is most of why a long
8697
+ // option list reads as noise.
8698
+ const widest = choices.reduce((w, c) => Math.max(w, c.label.length), 0);
8653
8699
  const frame = () => {
8654
8700
  const rows = choices.map((c, i) => {
8701
+ var _a;
8655
8702
  const here = i === state.cursor;
8656
- const mark = multi ? (state.selected.has(i) ? green("◉") : dim("◯")) : here ? green("❯") : " ";
8703
+ // Focus and selection are SEPARATE signals and both need to be visible: the caret says
8704
+ // where you are, the box says what's chosen. Showing only one (as this did) leaves a
8705
+ // multi-select where you can't tell what the keys will act on.
8706
+ const caret = here ? green("❯") : " ";
8707
+ const box = multi ? (state.selected.has(i) ? green("[✓]") : dim("[ ]")) : "";
8657
8708
  const name = here ? bold(c.label) : c.label;
8658
- const hint = c.hint ? ` ${dim(`— ${c.hint}`)}` : "";
8659
- return ` ${mark} ${name}${hint}`;
8709
+ const pad = " ".repeat(Math.max(0, widest - c.label.length));
8710
+ const blocks = ((_a = c.swatches) !== null && _a !== void 0 ? _a : []).map(s => swatch(s)).filter(Boolean).join(" ");
8711
+ const art = blocks ? ` ${blocks}` : "";
8712
+ const hint = c.hint ? ` ${dim(c.hint)}` : "";
8713
+ return ` ${caret} ${box ? `${box} ` : ""}${name}${pad}${art}${hint}`;
8660
8714
  });
8661
8715
  return [`${cyan("?")} ${bold(label)} ${dim(help)}`, ...rows].join("\n");
8662
8716
  };
@@ -8762,6 +8816,19 @@ class Prompter {
8762
8816
  }
8763
8817
  }
8764
8818
 
8819
+ /**
8820
+ * The hues a scheme derives from a seed — the same rotations the generator will bake, so the
8821
+ * preview beside each option is the palette you'd actually get, not an illustration.
8822
+ */
8823
+ const SCHEME_ROTATIONS = {
8824
+ complement: [180],
8825
+ analogous: [-30, 30],
8826
+ "split-complement": [150, 210],
8827
+ triadic: [120, 240],
8828
+ tetradic: [90, 180, 270],
8829
+ pentadic: [72, 144, 216, 288],
8830
+ };
8831
+ const schemeHues = (seedHex, scheme) => { var _a; return ((_a = SCHEME_ROTATIONS[scheme]) !== null && _a !== void 0 ? _a : []).map(deg => convertRgbToHex(parseColor(rotateHue(seedHex, deg)).rgb)); };
8765
8832
  /** What each harmony scheme is for, in one line — the names mean nothing to most people. */
8766
8833
  const SCHEME_HINTS = {
8767
8834
  complement: "one companion, 180° opposite — maximum separation",
@@ -8804,7 +8871,9 @@ async function promptCreateAnswers(p, given = {}) {
8804
8871
  var _a, _b, _c, _d, _e, _f, _g, _h;
8805
8872
  const seed = (_a = given.seed) !== null && _a !== void 0 ? _a : (await p.text("Primary colour", "#4c6ef5", isColor));
8806
8873
  const lightness = Math.round(rgbToOklch(parseColor(seed).rgb).L * 10) / 10;
8807
- p.write(` ${dim(`parsed · lightness ${lightness}% — lands at ≈${nearestLadderStep(lightness)} on the ladder`)}`);
8874
+ const seedHex = convertRgbToHex(parseColor(seed).rgb);
8875
+ const chip = swatch(seedHex, 3);
8876
+ p.write(` ${chip ? `${chip} ` : ""}${dim(`${seedHex} · lightness ${lightness}% — lands at ≈${nearestLadderStep(lightness)} on the ladder`)}`);
8808
8877
  p.write();
8809
8878
  const manual = Boolean(given.manual);
8810
8879
  let brandCount = 2;
@@ -8827,7 +8896,14 @@ async function promptCreateAnswers(p, given = {}) {
8827
8896
  const options = schemesFor(brandCount);
8828
8897
  scheme = (_c = given.scheme) !== null && _c !== void 0 ? _c : defaultSchemeFor(brandCount);
8829
8898
  if (!given.scheme && options.length > 1) {
8830
- scheme = await p.select("Harmony scheme", options.map(s => ({ value: s, label: s, hint: SCHEME_HINTS[s] })), Math.max(0, options.indexOf(scheme)));
8899
+ // Show the hues each scheme actually produces. "split-complement" is a word; two blocks of
8900
+ // real colour beside the seed is the thing you're choosing between.
8901
+ scheme = await p.select("Harmony scheme", options.map(s => ({
8902
+ value: s,
8903
+ label: s,
8904
+ hint: SCHEME_HINTS[s],
8905
+ swatches: [seedHex, ...schemeHues(seedHex, s)],
8906
+ })), Math.max(0, options.indexOf(scheme)));
8831
8907
  }
8832
8908
  if (scheme && brandCount > 1) {
8833
8909
  p.write(` ${dim(`→ ${scheme} · each member becomes its own palette with a full ladder`)}`);