@theme-registry/refract 0.1.3 → 0.1.5
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/build/prompt.d.ts +61 -8
- package/dist/build.cjs.js +1 -1
- package/dist/build.cjs.js.map +1 -1
- package/dist/build.esm.js +1 -1
- package/dist/build.esm.js.map +1 -1
- package/dist/cli.js +303 -26
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8004,7 +8004,7 @@ const RATIO_VALUES = {
|
|
|
8004
8004
|
* overlap, so a scaffolded literal and an authored `harmony:` key agree. `pentadic` is local — the
|
|
8005
8005
|
* subsystem has no five-way scheme, and the scaffolder needs one to reach five brand colours.
|
|
8006
8006
|
*/
|
|
8007
|
-
const SCHEME_ROTATIONS = {
|
|
8007
|
+
const SCHEME_ROTATIONS$1 = {
|
|
8008
8008
|
complement: [180],
|
|
8009
8009
|
analogous: [-30, 30],
|
|
8010
8010
|
"split-complement": [150, 210],
|
|
@@ -8106,7 +8106,7 @@ const defaultSchemeFor = (brandCount) => {
|
|
|
8106
8106
|
return "pentadic";
|
|
8107
8107
|
};
|
|
8108
8108
|
/** 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);
|
|
8109
|
+
const schemesFor = (brandCount) => Object.keys(SCHEME_ROTATIONS$1).filter((s) => SCHEME_ROTATIONS$1[s].length === brandCount - 1);
|
|
8110
8110
|
/**
|
|
8111
8111
|
* The contrast gate. Builds the palette set, audits every text-on-base pairing, and walks the
|
|
8112
8112
|
* lightness of each failing colour down one OKLCH point at a time until it clears the bar.
|
|
@@ -8202,7 +8202,7 @@ function scaffoldTheme(answers) {
|
|
|
8202
8202
|
}
|
|
8203
8203
|
else if (brandCount > 1) {
|
|
8204
8204
|
const scheme = (_g = answers.scheme) !== null && _g !== void 0 ? _g : defaultSchemeFor(brandCount);
|
|
8205
|
-
const rotations = SCHEME_ROTATIONS[scheme];
|
|
8205
|
+
const rotations = SCHEME_ROTATIONS$1[scheme];
|
|
8206
8206
|
if (!rotations)
|
|
8207
8207
|
throw new Error(`Unknown harmony scheme "${scheme}".`);
|
|
8208
8208
|
rotations.slice(0, brandCount - 1).forEach((deg, i) => {
|
|
@@ -8430,16 +8430,22 @@ function runCreate(options) {
|
|
|
8430
8430
|
}
|
|
8431
8431
|
|
|
8432
8432
|
/**
|
|
8433
|
-
*
|
|
8433
|
+
* Interactive prompts over `node:readline` and raw stdin (Node-only).
|
|
8434
8434
|
*
|
|
8435
8435
|
* The package ships zero runtime dependencies and this is the build layer, so rather than pull in a
|
|
8436
|
-
* prompt library
|
|
8437
|
-
*
|
|
8438
|
-
*
|
|
8439
|
-
*
|
|
8436
|
+
* prompt library these are the shapes the CLIs actually need, implemented directly.
|
|
8437
|
+
*
|
|
8438
|
+
* Three tiers, chosen per call:
|
|
8439
|
+
*
|
|
8440
|
+
* 1. **Raw mode** (a TTY that grants `setRawMode`) — arrow keys move, space toggles, Enter confirms.
|
|
8441
|
+
* What people expect from `npm create`.
|
|
8442
|
+
* 2. **Line mode** (a TTY that refuses raw mode — some CI shells, some remote terminals) — the same
|
|
8443
|
+
* questions answered by typing a number. Plainer, but never wedged.
|
|
8444
|
+
* 3. **Non-interactive** (no TTY at all: a pipe, CI, `--yes`) — every prompt takes its default
|
|
8445
|
+
* without blocking, so a scripted run can't deadlock on stdin that will never arrive.
|
|
8440
8446
|
*
|
|
8441
|
-
*
|
|
8442
|
-
*
|
|
8447
|
+
* Key handling is a **pure reducer** (`applyKey`), so navigation is unit-testable without a terminal;
|
|
8448
|
+
* the raw loop is only plumbing around it.
|
|
8443
8449
|
*/
|
|
8444
8450
|
/** ANSI helpers — no-ops when the stream isn't a TTY, so piped output stays clean. */
|
|
8445
8451
|
const useColor = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
@@ -8449,10 +8455,131 @@ const dim = (s) => paint("2", s);
|
|
|
8449
8455
|
const cyan = (s) => paint("36", s);
|
|
8450
8456
|
const green = (s) => paint("32", s);
|
|
8451
8457
|
const yellow = (s) => paint("33", s);
|
|
8458
|
+
/** Terminal control, named so the rendering below reads as intent rather than escape soup. */
|
|
8459
|
+
const cursorUp = (n) => (n > 0 ? `\u001b[${n}A` : "");
|
|
8460
|
+
const CLEAR_DOWN = "\u001b[0J";
|
|
8461
|
+
const HIDE_CURSOR = "\u001b[?25l";
|
|
8462
|
+
const SHOW_CURSOR = "\u001b[?25h";
|
|
8463
|
+
/**
|
|
8464
|
+
* Does this terminal do 24-bit colour? `COLORTERM` is the only real signal, and plenty of capable
|
|
8465
|
+
* terminals (Apple Terminal among them) never set it — so a missing value means "approximate", not
|
|
8466
|
+
* "give up". Gating swatches on truecolor alone would silently delete them for those users.
|
|
8467
|
+
*/
|
|
8468
|
+
const truecolor = () => { var _a; return /truecolor|24bit/i.test((_a = process.env.COLORTERM) !== null && _a !== void 0 ? _a : ""); };
|
|
8469
|
+
/** Nearest xterm-256 colour-cube index for an RGB triple — the fallback when 24-bit isn't offered. */
|
|
8470
|
+
const to256 = (r, g, b) => {
|
|
8471
|
+
const axis = (v) => (v < 48 ? 0 : v < 114 ? 1 : Math.min(5, Math.round((v - 35) / 40)));
|
|
8472
|
+
return 16 + 36 * axis(r) + 6 * axis(g) + axis(b);
|
|
8473
|
+
};
|
|
8474
|
+
/**
|
|
8475
|
+
* `#rrggbb` → a coloured block: exact in 24-bit terminals, approximated in 256-colour ones, and
|
|
8476
|
+
* empty when there's no colour at all (a pipe, `NO_COLOR`) so redirected output stays clean.
|
|
8477
|
+
*
|
|
8478
|
+
* Approximating is right here — the swatch is a preview, and the exact value is written to the theme
|
|
8479
|
+
* file regardless. An approximately-right hue beats a blank space.
|
|
8480
|
+
*/
|
|
8481
|
+
function swatch(hex, width = 2) {
|
|
8482
|
+
if (!useColor())
|
|
8483
|
+
return "";
|
|
8484
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
8485
|
+
if (!m)
|
|
8486
|
+
return "";
|
|
8487
|
+
const n = parseInt(m[1], 16);
|
|
8488
|
+
const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
8489
|
+
const bg = truecolor() ? `48;2;${r};${g};${b}` : `48;5;${to256(r, g, b)}`;
|
|
8490
|
+
return `\u001b[${bg}m${" ".repeat(width)}\u001b[0m`;
|
|
8491
|
+
}
|
|
8492
|
+
/** Decode a raw stdin chunk into a {@link ListKey}. Arrows arrive as escape sequences; j/k mirror vim. */
|
|
8493
|
+
function decodeKey(chunk) {
|
|
8494
|
+
switch (chunk) {
|
|
8495
|
+
case "\u001b[A":
|
|
8496
|
+
case "k":
|
|
8497
|
+
return "up";
|
|
8498
|
+
case "\u001b[B":
|
|
8499
|
+
case "j":
|
|
8500
|
+
return "down";
|
|
8501
|
+
case " ":
|
|
8502
|
+
return "space";
|
|
8503
|
+
case "\r":
|
|
8504
|
+
case "\n":
|
|
8505
|
+
return "submit";
|
|
8506
|
+
case "a":
|
|
8507
|
+
return "all";
|
|
8508
|
+
case "\u0003": // Ctrl-C
|
|
8509
|
+
case "\u001b": // bare Escape
|
|
8510
|
+
return "cancel";
|
|
8511
|
+
default:
|
|
8512
|
+
return "none";
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8452
8515
|
/**
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8455
|
-
*
|
|
8516
|
+
* Split one stdin chunk into the keys it contains.
|
|
8517
|
+
*
|
|
8518
|
+
* A held-down arrow key, or fast typing, delivers several sequences in a single `data` event — so
|
|
8519
|
+
* decoding the chunk as one key would swallow all but the first and make the list feel like it drops
|
|
8520
|
+
* input. Escape sequences (`ESC [ <letter>`) are taken as a unit; everything else is one key each.
|
|
8521
|
+
*/
|
|
8522
|
+
function decodeKeys(chunk) {
|
|
8523
|
+
const keys = [];
|
|
8524
|
+
for (let i = 0; i < chunk.length;) {
|
|
8525
|
+
if (chunk[i] === "\u001b" && chunk[i + 1] === "[" && i + 2 < chunk.length) {
|
|
8526
|
+
keys.push(decodeKey(chunk.slice(i, i + 3)));
|
|
8527
|
+
i += 3;
|
|
8528
|
+
}
|
|
8529
|
+
else {
|
|
8530
|
+
keys.push(decodeKey(chunk[i]));
|
|
8531
|
+
i += 1;
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
return keys;
|
|
8535
|
+
}
|
|
8536
|
+
/**
|
|
8537
|
+
* Advance a list prompt by one key. Pure — no I/O — so navigation is testable without a terminal.
|
|
8538
|
+
*
|
|
8539
|
+
* The cursor **wraps** at both ends: with six options, up from the first should land on the last
|
|
8540
|
+
* rather than stick. `space` and `a` toggle only in multi mode; in single mode Enter is the commit,
|
|
8541
|
+
* so space would be ambiguous.
|
|
8542
|
+
*/
|
|
8543
|
+
function applyKey(state, key, count, multi) {
|
|
8544
|
+
if (state.done || state.cancelled || count === 0)
|
|
8545
|
+
return state;
|
|
8546
|
+
switch (key) {
|
|
8547
|
+
case "up":
|
|
8548
|
+
return { ...state, cursor: (state.cursor - 1 + count) % count };
|
|
8549
|
+
case "down":
|
|
8550
|
+
return { ...state, cursor: (state.cursor + 1) % count };
|
|
8551
|
+
case "space": {
|
|
8552
|
+
if (!multi)
|
|
8553
|
+
return state;
|
|
8554
|
+
const next = new Set(state.selected);
|
|
8555
|
+
if (next.has(state.cursor))
|
|
8556
|
+
next.delete(state.cursor);
|
|
8557
|
+
else
|
|
8558
|
+
next.add(state.cursor);
|
|
8559
|
+
return { ...state, selected: next };
|
|
8560
|
+
}
|
|
8561
|
+
case "all": {
|
|
8562
|
+
if (!multi)
|
|
8563
|
+
return state;
|
|
8564
|
+
const everything = state.selected.size === count;
|
|
8565
|
+
return {
|
|
8566
|
+
...state,
|
|
8567
|
+
selected: everything ? new Set() : new Set(Array.from({ length: count }, (_, i) => i)),
|
|
8568
|
+
};
|
|
8569
|
+
}
|
|
8570
|
+
case "submit":
|
|
8571
|
+
return { ...state, done: true };
|
|
8572
|
+
case "cancel":
|
|
8573
|
+
return { ...state, cancelled: true };
|
|
8574
|
+
default:
|
|
8575
|
+
return state;
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
/** Can we drive the terminal directly? Some TTYs (and most CI) don't grant raw mode. */
|
|
8579
|
+
const rawCapable = () => Boolean(process.stdin.isTTY) && typeof process.stdin.setRawMode === "function";
|
|
8580
|
+
/**
|
|
8581
|
+
* A prompt session. Holds at most one readline interface, so stdin is opened and closed once;
|
|
8582
|
+
* `interactive` is false when there's no TTY, and every ask short-circuits to its default.
|
|
8456
8583
|
*/
|
|
8457
8584
|
class Prompter {
|
|
8458
8585
|
constructor(interactive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY)) {
|
|
@@ -8471,7 +8598,11 @@ class Prompter {
|
|
|
8471
8598
|
question(text) {
|
|
8472
8599
|
return new Promise(resolve => this.io.question(text, answer => resolve(answer)));
|
|
8473
8600
|
}
|
|
8474
|
-
/**
|
|
8601
|
+
/** Raw write, no newline — for cursor control during a live render. */
|
|
8602
|
+
out(s) {
|
|
8603
|
+
process.stdout.write(s);
|
|
8604
|
+
}
|
|
8605
|
+
/** Print a line. Kept on the prompter so command code never touches `process.stdout`. */
|
|
8475
8606
|
write(line = "") {
|
|
8476
8607
|
process.stdout.write(`${line}\n`);
|
|
8477
8608
|
}
|
|
@@ -8498,11 +8629,140 @@ class Prompter {
|
|
|
8498
8629
|
});
|
|
8499
8630
|
return Number(answer);
|
|
8500
8631
|
}
|
|
8501
|
-
/** Pick one
|
|
8632
|
+
/** Pick one — arrows to move, Enter to choose. */
|
|
8502
8633
|
async select(label, choices, defaultIndex = 0) {
|
|
8503
|
-
var _a, _b;
|
|
8634
|
+
var _a, _b, _c;
|
|
8504
8635
|
if (!this.interactive || choices.length === 1)
|
|
8505
8636
|
return (_b = (_a = choices[defaultIndex]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : choices[0].value;
|
|
8637
|
+
if (rawCapable()) {
|
|
8638
|
+
const picked = await this.runList(label, choices, {
|
|
8639
|
+
multi: false,
|
|
8640
|
+
cursor: defaultIndex,
|
|
8641
|
+
selected: new Set(),
|
|
8642
|
+
});
|
|
8643
|
+
return choices[(_c = picked[0]) !== null && _c !== void 0 ? _c : defaultIndex].value;
|
|
8644
|
+
}
|
|
8645
|
+
return this.selectByNumber(label, choices, defaultIndex);
|
|
8646
|
+
}
|
|
8647
|
+
/** Pick any — arrows to move, space to toggle, `a` for all/none, Enter to confirm. */
|
|
8648
|
+
async multiselect(label, choices, preselected) {
|
|
8649
|
+
var _a;
|
|
8650
|
+
if (!this.interactive)
|
|
8651
|
+
return preselected.map(i => choices[i].value);
|
|
8652
|
+
if (rawCapable()) {
|
|
8653
|
+
const picked = await this.runList(label, choices, {
|
|
8654
|
+
multi: true,
|
|
8655
|
+
cursor: (_a = preselected[0]) !== null && _a !== void 0 ? _a : 0,
|
|
8656
|
+
selected: new Set(preselected),
|
|
8657
|
+
});
|
|
8658
|
+
return picked.map(i => choices[i].value);
|
|
8659
|
+
}
|
|
8660
|
+
return this.multiselectByNumber(label, choices, preselected);
|
|
8661
|
+
}
|
|
8662
|
+
/**
|
|
8663
|
+
* The raw-mode list loop. Renders in place: each keypress rewinds over the block just drawn and
|
|
8664
|
+
* repaints it, so the list stays put instead of scrolling the terminal away.
|
|
8665
|
+
*
|
|
8666
|
+
* Any readline interface is closed first — it would otherwise swallow the keystrokes we need.
|
|
8667
|
+
* `cleanup` always restores the terminal (raw mode off, cursor shown), including on cancel, so a
|
|
8668
|
+
* Ctrl-C can't leave the shell in a state where typing is invisible.
|
|
8669
|
+
*/
|
|
8670
|
+
runList(label, choices, init) {
|
|
8671
|
+
this.close(); // readline and raw mode can't both own stdin
|
|
8672
|
+
const stdin = process.stdin;
|
|
8673
|
+
const { multi } = init;
|
|
8674
|
+
let state = {
|
|
8675
|
+
cursor: Math.max(0, Math.min(init.cursor, choices.length - 1)),
|
|
8676
|
+
selected: init.selected,
|
|
8677
|
+
done: false,
|
|
8678
|
+
cancelled: false,
|
|
8679
|
+
};
|
|
8680
|
+
let painted = 0;
|
|
8681
|
+
const help = multi ? "↑↓ move · space toggle · a all · enter confirm" : "↑↓ move · enter select";
|
|
8682
|
+
// Pad labels so the hints line up into a column — a ragged right edge is most of why a long
|
|
8683
|
+
// option list reads as noise.
|
|
8684
|
+
const widest = choices.reduce((w, c) => Math.max(w, c.label.length), 0);
|
|
8685
|
+
const frame = () => {
|
|
8686
|
+
const rows = choices.map((c, i) => {
|
|
8687
|
+
var _a;
|
|
8688
|
+
const here = i === state.cursor;
|
|
8689
|
+
// Focus and selection are SEPARATE signals and both need to be visible: the caret says
|
|
8690
|
+
// where you are, the box says what's chosen. Showing only one (as this did) leaves a
|
|
8691
|
+
// multi-select where you can't tell what the keys will act on.
|
|
8692
|
+
const caret = here ? green("❯") : " ";
|
|
8693
|
+
const box = multi ? (state.selected.has(i) ? green("[✓]") : dim("[ ]")) : "";
|
|
8694
|
+
const name = here ? bold(c.label) : c.label;
|
|
8695
|
+
const pad = " ".repeat(Math.max(0, widest - c.label.length));
|
|
8696
|
+
const blocks = ((_a = c.swatches) !== null && _a !== void 0 ? _a : []).map(s => swatch(s)).filter(Boolean).join(" ");
|
|
8697
|
+
const art = blocks ? ` ${blocks}` : "";
|
|
8698
|
+
const hint = c.hint ? ` ${dim(c.hint)}` : "";
|
|
8699
|
+
return ` ${caret} ${box ? `${box} ` : ""}${name}${pad}${art}${hint}`;
|
|
8700
|
+
});
|
|
8701
|
+
return [`${cyan("?")} ${bold(label)} ${dim(help)}`, ...rows].join("\n");
|
|
8702
|
+
};
|
|
8703
|
+
const paintFrame = () => {
|
|
8704
|
+
if (painted)
|
|
8705
|
+
this.out(cursorUp(painted) + CLEAR_DOWN);
|
|
8706
|
+
const text = frame();
|
|
8707
|
+
this.out(`${text}\n`);
|
|
8708
|
+
painted = text.split("\n").length;
|
|
8709
|
+
};
|
|
8710
|
+
return new Promise((resolve, reject) => {
|
|
8711
|
+
const cleanup = () => {
|
|
8712
|
+
stdin.removeListener("data", onData);
|
|
8713
|
+
stdin.removeListener("end", onEnd);
|
|
8714
|
+
if (stdin.isTTY)
|
|
8715
|
+
stdin.setRawMode(false);
|
|
8716
|
+
stdin.pause();
|
|
8717
|
+
this.out(SHOW_CURSOR);
|
|
8718
|
+
};
|
|
8719
|
+
/**
|
|
8720
|
+
* stdin closed while we were waiting — a piped run, a closed terminal, a killed parent. There
|
|
8721
|
+
* is no further input coming, so commit what's on screen instead of blocking forever.
|
|
8722
|
+
*/
|
|
8723
|
+
const onEnd = () => {
|
|
8724
|
+
const chosen = multi ? [...state.selected].sort((a, b) => a - b) : [state.cursor];
|
|
8725
|
+
this.out(cursorUp(painted) + CLEAR_DOWN);
|
|
8726
|
+
cleanup();
|
|
8727
|
+
resolve(chosen);
|
|
8728
|
+
};
|
|
8729
|
+
const onData = (chunk) => {
|
|
8730
|
+
// One event can carry several keys (a held-down arrow); apply them all, then paint once.
|
|
8731
|
+
for (const key of decodeKeys(chunk)) {
|
|
8732
|
+
state = applyKey(state, key, choices.length, multi);
|
|
8733
|
+
if (state.done || state.cancelled)
|
|
8734
|
+
break;
|
|
8735
|
+
}
|
|
8736
|
+
if (state.cancelled) {
|
|
8737
|
+
this.out(cursorUp(painted) + CLEAR_DOWN);
|
|
8738
|
+
cleanup();
|
|
8739
|
+
reject(new Error("Cancelled."));
|
|
8740
|
+
return;
|
|
8741
|
+
}
|
|
8742
|
+
if (state.done) {
|
|
8743
|
+
const chosen = multi ? [...state.selected].sort((a, b) => a - b) : [state.cursor];
|
|
8744
|
+
// Replace the live block with a one-line record of the answer, so a finished interview
|
|
8745
|
+
// reads back as a transcript rather than a wall of spent menus.
|
|
8746
|
+
this.out(cursorUp(painted) + CLEAR_DOWN);
|
|
8747
|
+
const summary = chosen.length ? chosen.map(i => choices[i].label).join(", ") : "none";
|
|
8748
|
+
this.write(`${cyan("?")} ${bold(label)} ${dim("›")} ${green(summary)}`);
|
|
8749
|
+
cleanup();
|
|
8750
|
+
resolve(chosen);
|
|
8751
|
+
return;
|
|
8752
|
+
}
|
|
8753
|
+
paintFrame();
|
|
8754
|
+
};
|
|
8755
|
+
stdin.setRawMode(true);
|
|
8756
|
+
stdin.resume();
|
|
8757
|
+
stdin.setEncoding("utf8");
|
|
8758
|
+
this.out(HIDE_CURSOR);
|
|
8759
|
+
stdin.on("data", onData);
|
|
8760
|
+
stdin.on("end", onEnd);
|
|
8761
|
+
paintFrame();
|
|
8762
|
+
});
|
|
8763
|
+
}
|
|
8764
|
+
/** Line-mode fallback: pick one by number. */
|
|
8765
|
+
async selectByNumber(label, choices, defaultIndex) {
|
|
8506
8766
|
this.write(`${cyan("?")} ${bold(label)}`);
|
|
8507
8767
|
choices.forEach((c, i) => {
|
|
8508
8768
|
const marker = i === defaultIndex ? green("❯") : " ";
|
|
@@ -8519,14 +8779,9 @@ class Prompter {
|
|
|
8519
8779
|
this.write(` ${yellow("!")} Enter a number between 1 and ${choices.length}.`);
|
|
8520
8780
|
}
|
|
8521
8781
|
}
|
|
8522
|
-
/**
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
*/
|
|
8526
|
-
async multiselect(label, choices, preselected) {
|
|
8527
|
-
if (!this.interactive)
|
|
8528
|
-
return preselected.map(i => choices[i].value);
|
|
8529
|
-
this.write(`${cyan("?")} ${bold(label)} ${dim("(comma-separated, blank = keep, \"none\" = clear)")}`);
|
|
8782
|
+
/** Line-mode fallback: pick any by comma-separated numbers. */
|
|
8783
|
+
async multiselectByNumber(label, choices, preselected) {
|
|
8784
|
+
this.write(`${cyan("?")} ${bold(label)} ${dim('(comma-separated, blank = keep, "none" = clear)')}`);
|
|
8530
8785
|
choices.forEach((c, i) => {
|
|
8531
8786
|
const on = preselected.includes(i);
|
|
8532
8787
|
const hint = c.hint ? ` ${dim(`— ${c.hint}`)}` : "";
|
|
@@ -8547,6 +8802,19 @@ class Prompter {
|
|
|
8547
8802
|
}
|
|
8548
8803
|
}
|
|
8549
8804
|
|
|
8805
|
+
/**
|
|
8806
|
+
* The hues a scheme derives from a seed — the same rotations the generator will bake, so the
|
|
8807
|
+
* preview beside each option is the palette you'd actually get, not an illustration.
|
|
8808
|
+
*/
|
|
8809
|
+
const SCHEME_ROTATIONS = {
|
|
8810
|
+
complement: [180],
|
|
8811
|
+
analogous: [-30, 30],
|
|
8812
|
+
"split-complement": [150, 210],
|
|
8813
|
+
triadic: [120, 240],
|
|
8814
|
+
tetradic: [90, 180, 270],
|
|
8815
|
+
pentadic: [72, 144, 216, 288],
|
|
8816
|
+
};
|
|
8817
|
+
const schemeHues = (seedHex, scheme) => { var _a; return ((_a = SCHEME_ROTATIONS[scheme]) !== null && _a !== void 0 ? _a : []).map(deg => convertRgbToHex(parseColor(rotateHue(seedHex, deg)).rgb)); };
|
|
8550
8818
|
/** What each harmony scheme is for, in one line — the names mean nothing to most people. */
|
|
8551
8819
|
const SCHEME_HINTS = {
|
|
8552
8820
|
complement: "one companion, 180° opposite — maximum separation",
|
|
@@ -8589,7 +8857,9 @@ async function promptCreateAnswers(p, given = {}) {
|
|
|
8589
8857
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
8590
8858
|
const seed = (_a = given.seed) !== null && _a !== void 0 ? _a : (await p.text("Primary colour", "#4c6ef5", isColor));
|
|
8591
8859
|
const lightness = Math.round(rgbToOklch(parseColor(seed).rgb).L * 10) / 10;
|
|
8592
|
-
|
|
8860
|
+
const seedHex = convertRgbToHex(parseColor(seed).rgb);
|
|
8861
|
+
const chip = swatch(seedHex, 3);
|
|
8862
|
+
p.write(` ${chip ? `${chip} ` : ""}${dim(`${seedHex} · lightness ${lightness}% — lands at ≈${nearestLadderStep(lightness)} on the ladder`)}`);
|
|
8593
8863
|
p.write();
|
|
8594
8864
|
const manual = Boolean(given.manual);
|
|
8595
8865
|
let brandCount = 2;
|
|
@@ -8612,7 +8882,14 @@ async function promptCreateAnswers(p, given = {}) {
|
|
|
8612
8882
|
const options = schemesFor(brandCount);
|
|
8613
8883
|
scheme = (_c = given.scheme) !== null && _c !== void 0 ? _c : defaultSchemeFor(brandCount);
|
|
8614
8884
|
if (!given.scheme && options.length > 1) {
|
|
8615
|
-
|
|
8885
|
+
// Show the hues each scheme actually produces. "split-complement" is a word; two blocks of
|
|
8886
|
+
// real colour beside the seed is the thing you're choosing between.
|
|
8887
|
+
scheme = await p.select("Harmony scheme", options.map(s => ({
|
|
8888
|
+
value: s,
|
|
8889
|
+
label: s,
|
|
8890
|
+
hint: SCHEME_HINTS[s],
|
|
8891
|
+
swatches: [seedHex, ...schemeHues(seedHex, s)],
|
|
8892
|
+
})), Math.max(0, options.indexOf(scheme)));
|
|
8616
8893
|
}
|
|
8617
8894
|
if (scheme && brandCount > 1) {
|
|
8618
8895
|
p.write(` ${dim(`→ ${scheme} · each member becomes its own palette with a full ladder`)}`);
|