synthesisui 0.16.55 → 0.16.57
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/commands/import.js +190 -4
- package/dist/doctor/color-distance.js +54 -0
- package/dist/index.js +6 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, join, relative } from "node:path";
|
|
3
3
|
import { readToken, resolveRegistry } from "../config.js";
|
|
4
|
-
import { isNearDuplicate } from "../doctor/color-distance.js";
|
|
4
|
+
import { isNearDuplicate, isNeutral, lightness, } from "../doctor/color-distance.js";
|
|
5
5
|
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
6
6
|
import { crosswalk, observedRules } from "../doctor/crosswalk.js";
|
|
7
7
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
@@ -327,13 +327,19 @@ export async function takeCensus(root) {
|
|
|
327
327
|
* by the two-option rule, while its type declares four sizes and three
|
|
328
328
|
* variants (dono, 31/07). The component would have reached the platform with
|
|
329
329
|
* no contract at all, which is the exact thing reading definitions was for.
|
|
330
|
+
*
|
|
331
|
+
* Carried BESIDE the usage rather than written into it. The first attempt
|
|
332
|
+
* merged the declaration into `props` and broke the two readings that need
|
|
333
|
+
* `props` to mean "what the code passed": the composition list started
|
|
334
|
+
* showing declared options as composed, and the dead-option check went quiet
|
|
335
|
+
* because every declared option now looked used.
|
|
330
336
|
*/
|
|
331
337
|
const declaredBy = new Map(defined.map((d) => [d.name, d]));
|
|
332
338
|
const enriched = inventory.map((c) => {
|
|
333
339
|
const d = declaredBy.get(c.name);
|
|
334
340
|
if (!d || Object.keys(d.axes).length === 0)
|
|
335
341
|
return c;
|
|
336
|
-
return { ...c,
|
|
342
|
+
return { ...c, declaredAxes: d.axes };
|
|
337
343
|
});
|
|
338
344
|
const merged = [...enriched, ...fromTypes];
|
|
339
345
|
// The verdict travels WITH the payload: the platform reads one reading rather
|
|
@@ -401,7 +407,12 @@ function summarize(c) {
|
|
|
401
407
|
// started using their own token table: not "you have drift" but "you have
|
|
402
408
|
// drift your own system already solved".
|
|
403
409
|
if (c.declaredAlt && Object.keys(c.declaredAlt).length > 0) {
|
|
404
|
-
console.log(body(
|
|
410
|
+
console.log(body(
|
|
411
|
+
// No promise here. Whether a second scheme actually travels depends on
|
|
412
|
+
// those declarations naming ROLES, which the platform decides - and a
|
|
413
|
+
// project with two aliased dark tokens got told "both schemes travel"
|
|
414
|
+
// and then received one (dono, 31/07).
|
|
415
|
+
`${Object.keys(c.declaredAlt).length} of them are declared again in a dark scope`));
|
|
405
416
|
}
|
|
406
417
|
// The inventory, phase one: what this project composes. Printed, never
|
|
407
418
|
// mapped - deciding that their `Pill` is our `ds-badge` is a judgement a
|
|
@@ -596,7 +607,8 @@ function printAgentContract() {
|
|
|
596
607
|
console.log(body(paint.faint(' "reading": {')));
|
|
597
608
|
console.log(body(paint.faint(' "roles": { "canvas": "#…", "foreground": "#…", "primary": "#…" },')));
|
|
598
609
|
console.log(body(paint.faint(' "fonts": { "display": "…", "body": "…" },')));
|
|
599
|
-
console.log(body(paint.faint(' "concept": "one paragraph on what this product is"')));
|
|
610
|
+
console.log(body(paint.faint(' "concept": "one paragraph on what this product is",')));
|
|
611
|
+
console.log(body(paint.faint(' "themes": { "default": "dark", "has": ["dark"] }')));
|
|
600
612
|
console.log(body(paint.faint(" }")));
|
|
601
613
|
console.log("");
|
|
602
614
|
console.log(body("Every hex in `roles` must be one the census already observed - a value"));
|
|
@@ -662,6 +674,33 @@ const GENERIC = new Set([
|
|
|
662
674
|
"shared",
|
|
663
675
|
"common",
|
|
664
676
|
]);
|
|
677
|
+
/**
|
|
678
|
+
* Which ends of the neutral ladder their own tokens reach.
|
|
679
|
+
*
|
|
680
|
+
* The bands match the ones the platform maps roles with: a surface needs to be
|
|
681
|
+
* a surface, and a near-black is the only thing that reads as a dark canvas.
|
|
682
|
+
* Both true means BOTH schemes can be built out of values they already
|
|
683
|
+
* declared - no colour borrowed, their token names intact - and the only open
|
|
684
|
+
* question is which one is the default. Exactly one true means the other mode
|
|
685
|
+
* would need colours they do not have, which is worth saying out loud rather
|
|
686
|
+
* than inventing.
|
|
687
|
+
*/
|
|
688
|
+
export function ladderReach(declared) {
|
|
689
|
+
let light = false;
|
|
690
|
+
let dark = false;
|
|
691
|
+
for (const value of Object.values(declared)) {
|
|
692
|
+
if (typeof value !== "string")
|
|
693
|
+
continue;
|
|
694
|
+
const L = lightness(value);
|
|
695
|
+
if (L == null || !isNeutral(value))
|
|
696
|
+
continue;
|
|
697
|
+
if (L > 0.93)
|
|
698
|
+
light = true;
|
|
699
|
+
if (L < 0.05)
|
|
700
|
+
dark = true;
|
|
701
|
+
}
|
|
702
|
+
return { light, dark };
|
|
703
|
+
}
|
|
665
704
|
export function deriveName(pkgName, dirName) {
|
|
666
705
|
const raw = (pkgName ?? "").trim();
|
|
667
706
|
const scoped = /^@([^/]+)\/(.+)$/.exec(raw);
|
|
@@ -701,6 +740,127 @@ async function askName(suggested) {
|
|
|
701
740
|
rl.close();
|
|
702
741
|
}
|
|
703
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Pick the default surface, by SELECTION rather than by typing (dono, 31/07).
|
|
745
|
+
*
|
|
746
|
+
* A one-bit answer typed as a word invites a typo that inverts a whole system,
|
|
747
|
+
* and a free-text prompt reads as "compose an answer" when the answer is a
|
|
748
|
+
* choice between two things we can already name. Arrows move, Enter takes what
|
|
749
|
+
* is highlighted; `1`/`2` and `d`/`l` work too, because muscle memory differs
|
|
750
|
+
* and none of them are ambiguous here.
|
|
751
|
+
*
|
|
752
|
+
* Never blocks without a person: no TTY means the caller's `--scheme` or the
|
|
753
|
+
* measured default stands.
|
|
754
|
+
*/
|
|
755
|
+
async function askScheme(suggested) {
|
|
756
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
757
|
+
return suggested;
|
|
758
|
+
const options = ["dark", "light"];
|
|
759
|
+
let at = options.indexOf(suggested);
|
|
760
|
+
if (at < 0)
|
|
761
|
+
at = 0;
|
|
762
|
+
console.log("");
|
|
763
|
+
console.log(section("Your ladder reaches both ends"));
|
|
764
|
+
console.log(body(paint.faint("Both schemes get built from tokens you already declare. Pick the default.")));
|
|
765
|
+
console.log("");
|
|
766
|
+
const render = (first) => {
|
|
767
|
+
if (!first)
|
|
768
|
+
process.stdout.write(`\u001b[${options.length}A`);
|
|
769
|
+
for (const [i, opt] of options.entries()) {
|
|
770
|
+
const label = opt === "dark" ? "dark" : "light";
|
|
771
|
+
const line = i === at
|
|
772
|
+
? ` ${paint.strong(`> ${label}`)}`
|
|
773
|
+
: ` ${paint.faint(` ${label}`)}`;
|
|
774
|
+
process.stdout.write(`\u001b[2K${line}\n`);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
render(true);
|
|
778
|
+
return await new Promise((resolve) => {
|
|
779
|
+
const stdin = process.stdin;
|
|
780
|
+
const done = (value) => {
|
|
781
|
+
stdin.off("data", onData);
|
|
782
|
+
if (stdin.isTTY)
|
|
783
|
+
stdin.setRawMode(false);
|
|
784
|
+
stdin.pause();
|
|
785
|
+
// Leave the choice on screen rather than a cursor parked mid-list.
|
|
786
|
+
at = options.indexOf(value);
|
|
787
|
+
render(false);
|
|
788
|
+
console.log("");
|
|
789
|
+
resolve(value);
|
|
790
|
+
};
|
|
791
|
+
const onData = (buf) => {
|
|
792
|
+
const key = buf.toString();
|
|
793
|
+
if (key === "\u0003") {
|
|
794
|
+
// Ctrl-C during a question is a person leaving, not a value to invent.
|
|
795
|
+
if (stdin.isTTY)
|
|
796
|
+
stdin.setRawMode(false);
|
|
797
|
+
process.exit(130);
|
|
798
|
+
}
|
|
799
|
+
if (key === "\u001b[A" || key === "k") {
|
|
800
|
+
at = (at + options.length - 1) % options.length;
|
|
801
|
+
render(false);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (key === "\u001b[B" || key === "j" || key === "\t") {
|
|
805
|
+
at = (at + 1) % options.length;
|
|
806
|
+
render(false);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (key === "1")
|
|
810
|
+
return done("dark");
|
|
811
|
+
if (key === "2")
|
|
812
|
+
return done("light");
|
|
813
|
+
if (key === "d" || key === "D")
|
|
814
|
+
return done("dark");
|
|
815
|
+
if (key === "l" || key === "L")
|
|
816
|
+
return done("light");
|
|
817
|
+
if (key === "\r" || key === "\n")
|
|
818
|
+
return done(options[at]);
|
|
819
|
+
};
|
|
820
|
+
stdin.setRawMode(true);
|
|
821
|
+
stdin.resume();
|
|
822
|
+
stdin.on("data", onData);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Which end to offer first when both are available.
|
|
827
|
+
*
|
|
828
|
+
* The ladder itself votes: a product built dark tends to declare more rungs at
|
|
829
|
+
* the dark end, because that is where its surfaces live and where it needs the
|
|
830
|
+
* separation. The real one (dono, 31/07) declares nine near-blacks and five
|
|
831
|
+
* light greys, and its dashboard is near-black - so the count agrees with the
|
|
832
|
+
* screen. It is only a default; the person confirms it either way.
|
|
833
|
+
*/
|
|
834
|
+
export function defaultScheme(declared) {
|
|
835
|
+
let dark = 0;
|
|
836
|
+
let light = 0;
|
|
837
|
+
for (const value of Object.values(declared)) {
|
|
838
|
+
if (typeof value !== "string" || !isNeutral(value))
|
|
839
|
+
continue;
|
|
840
|
+
const L = lightness(value);
|
|
841
|
+
if (L == null)
|
|
842
|
+
continue;
|
|
843
|
+
if (L < 0.5)
|
|
844
|
+
dark++;
|
|
845
|
+
else
|
|
846
|
+
light++;
|
|
847
|
+
}
|
|
848
|
+
return dark > light ? "dark" : "light";
|
|
849
|
+
}
|
|
850
|
+
/** On a dry run there is nobody to ask, so say what was measured. */
|
|
851
|
+
function sayReach(c) {
|
|
852
|
+
const reach = ladderReach(c.declared);
|
|
853
|
+
console.log("");
|
|
854
|
+
if (reach.light && reach.dark) {
|
|
855
|
+
console.log(body(`Your neutrals reach both ends, so light and dark could both be built from your own tokens - but only your project knows which it actually ships. Answer ${paint.strong("themes")} below, or let the real run ask; the ladder alone would guess ${paint.strong(defaultScheme(c.declared))}.`));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (reach.dark || reach.light) {
|
|
859
|
+
const has = reach.dark ? "dark" : "light";
|
|
860
|
+
const missing = reach.dark ? "light" : "dark";
|
|
861
|
+
console.log(body(`Your neutrals only reach the ${has} end, so this system gets one scheme. A ${missing} mode would need surfaces you do not declare.`));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
704
864
|
export async function runImport(opts) {
|
|
705
865
|
const root = opts.root ?? process.cwd();
|
|
706
866
|
// A census handed to us (an agent annotated it) is sent as-is; the numbers
|
|
@@ -741,6 +901,7 @@ export async function runImport(opts) {
|
|
|
741
901
|
console.log(body(`Written to ${paint.strong(relative(root, out))}`));
|
|
742
902
|
if (opts.dry) {
|
|
743
903
|
console.log(body("Nothing was sent. Read the file, then run it without --dry."));
|
|
904
|
+
sayReach(census);
|
|
744
905
|
printComponents(census);
|
|
745
906
|
printAgentContract();
|
|
746
907
|
console.log("");
|
|
@@ -748,6 +909,31 @@ export async function runImport(opts) {
|
|
|
748
909
|
}
|
|
749
910
|
const suggested = deriveName(census.project.name, basename(root) || "system");
|
|
750
911
|
const chosen = opts.name?.trim() || (await askName(suggested));
|
|
912
|
+
const reach = ladderReach(census.declared);
|
|
913
|
+
// An explicit flag is a decision already made. Otherwise the person answers,
|
|
914
|
+
// starting from the agent's reading if there is one and from the ladder's own
|
|
915
|
+
// vote if there is not - and with nobody at a terminal, that same order
|
|
916
|
+
// stands unattended.
|
|
917
|
+
const read = census.reading?.themes;
|
|
918
|
+
const scheme = opts.scheme ??
|
|
919
|
+
read?.default ??
|
|
920
|
+
// Only ask when nobody has looked. A reading that names one theme has
|
|
921
|
+
// already answered this, and a second question would just be a chance to
|
|
922
|
+
// contradict it.
|
|
923
|
+
(read?.has?.length === 1
|
|
924
|
+
? read.has[0]
|
|
925
|
+
: reach.light && reach.dark
|
|
926
|
+
? await askScheme(defaultScheme(census.declared))
|
|
927
|
+
: reach.dark
|
|
928
|
+
? "dark"
|
|
929
|
+
: reach.light
|
|
930
|
+
? "light"
|
|
931
|
+
: undefined);
|
|
932
|
+
if (scheme)
|
|
933
|
+
census.scheme = scheme;
|
|
934
|
+
// The file on disk has to say what we sent, or the next person to read it is
|
|
935
|
+
// reading a different import than the one that happened.
|
|
936
|
+
await writeFile(out, `${JSON.stringify(census, null, 2)}\n`, "utf8");
|
|
751
937
|
const token = await readToken();
|
|
752
938
|
if (!token) {
|
|
753
939
|
console.log("");
|
|
@@ -68,3 +68,57 @@ export function isNearDuplicate(a, b, threshold = JND) {
|
|
|
68
68
|
const d = deltaE(a, b);
|
|
69
69
|
return d != null && d <= threshold;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* LIGHTNESS and NEUTRAL - carried here for the same reason the ΔE maths is:
|
|
73
|
+
* the CLI is a standalone package and cannot import from the app. These are the
|
|
74
|
+
* platform's `lightness` and its neutral test (`apps/web/src/lib/ds/census-import.ts`)
|
|
75
|
+
* to the digit, and the spec asserts the same landmarks on both sides, because
|
|
76
|
+
* the two halves deciding differently what counts as a dark canvas is worse
|
|
77
|
+
* than either of them being wrong.
|
|
78
|
+
*/
|
|
79
|
+
export function lightness(hex) {
|
|
80
|
+
const h = hex.trim().replace("#", "");
|
|
81
|
+
const full = h.length === 3 || h.length === 4
|
|
82
|
+
? h
|
|
83
|
+
.slice(0, 3)
|
|
84
|
+
.split("")
|
|
85
|
+
.map((c) => c + c)
|
|
86
|
+
.join("")
|
|
87
|
+
: h.slice(0, 6);
|
|
88
|
+
if (full.length !== 6)
|
|
89
|
+
return null;
|
|
90
|
+
const n = Number.parseInt(full, 16);
|
|
91
|
+
if (Number.isNaN(n))
|
|
92
|
+
return null;
|
|
93
|
+
return ((0.2126 * ((n >> 16) & 255) +
|
|
94
|
+
0.7152 * ((n >> 8) & 255) +
|
|
95
|
+
0.0722 * (n & 255)) /
|
|
96
|
+
255);
|
|
97
|
+
}
|
|
98
|
+
/** Channel spread, not HSL saturation: `#f9fbfd` is 0.50 saturated and 0.016
|
|
99
|
+
* chromatic, and treating it as coloured throws every off-white surface out of
|
|
100
|
+
* the neutral ladder. */
|
|
101
|
+
export function chroma(hex) {
|
|
102
|
+
const h = hex.trim().replace("#", "");
|
|
103
|
+
const full = h.length === 3 || h.length === 4
|
|
104
|
+
? h
|
|
105
|
+
.slice(0, 3)
|
|
106
|
+
.split("")
|
|
107
|
+
.map((c) => c + c)
|
|
108
|
+
.join("")
|
|
109
|
+
: h.slice(0, 6);
|
|
110
|
+
if (full.length !== 6)
|
|
111
|
+
return null;
|
|
112
|
+
const n = Number.parseInt(full, 16);
|
|
113
|
+
if (Number.isNaN(n))
|
|
114
|
+
return null;
|
|
115
|
+
const r = (n >> 16) & 255;
|
|
116
|
+
const g = (n >> 8) & 255;
|
|
117
|
+
const b = n & 255;
|
|
118
|
+
return (Math.max(r, g, b) - Math.min(r, g, b)) / 255;
|
|
119
|
+
}
|
|
120
|
+
export const NEUTRAL_CHROMA = 0.1;
|
|
121
|
+
export function isNeutral(hex) {
|
|
122
|
+
const c = chroma(hex);
|
|
123
|
+
return c != null && c < NEUTRAL_CHROMA;
|
|
124
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,7 @@ Options:
|
|
|
66
66
|
--version <n> install a specific version (default: latest)
|
|
67
67
|
--ds <slug> init: bring this DS in right away · generate: target DS (default: installed)
|
|
68
68
|
--name <name> preferred component name for generate
|
|
69
|
+
--scheme <s> dark|light - which end of your ladder is the default
|
|
69
70
|
--target <t> template/init target: next | general (default: next)
|
|
70
71
|
--pages-dir <dir> init: folder for generated pages (default: app)
|
|
71
72
|
--components-dir <dir> init: folder where components live (default: components)
|
|
@@ -159,6 +160,11 @@ async function main() {
|
|
|
159
160
|
dry: flags.dry === true,
|
|
160
161
|
census: typeof flags.census === "string" ? flags.census : undefined,
|
|
161
162
|
name: typeof flags.name === "string" ? flags.name : undefined,
|
|
163
|
+
// Only these two words. Anything else is a typo that would silently
|
|
164
|
+
// invert a system, so it falls through to being measured and asked.
|
|
165
|
+
scheme: flags.scheme === "dark" || flags.scheme === "light"
|
|
166
|
+
? flags.scheme
|
|
167
|
+
: undefined,
|
|
162
168
|
registry,
|
|
163
169
|
});
|
|
164
170
|
break;
|
package/package.json
CHANGED