synthesisui 0.16.104 → 0.16.106
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/anatomy-read.js +43 -5
- package/dist/commands/import.js +30 -3
- package/dist/commands/mcp.js +8 -0
- package/dist/doctor/transcribe.js +125 -78
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -145,6 +145,12 @@ root,
|
|
|
145
145
|
/** This component's sketch, so a node can name its classes by INDEX instead
|
|
146
146
|
* of retyping them - see `AnatomyRead.at`. */
|
|
147
147
|
sketch) {
|
|
148
|
+
/**
|
|
149
|
+
* THE PACKAGE THE RETURNED ELEMENT CAME FROM, measured rather than spelled.
|
|
150
|
+
* Node 0 of the sketch IS what the component returns, and the reader stamped its
|
|
151
|
+
* import specifier there - see `SketchNode.from`.
|
|
152
|
+
*/
|
|
153
|
+
const rootPackage = sketch?.[0]?.from ?? null;
|
|
148
154
|
const parts = {};
|
|
149
155
|
const composes = [];
|
|
150
156
|
const external = [];
|
|
@@ -342,20 +348,52 @@ sketch) {
|
|
|
342
348
|
* `from` says it: the PACKAGE disambiguates what the name cannot. It wins over
|
|
343
349
|
* the name, because a caller that sends both is telling us where it came from.
|
|
344
350
|
*/
|
|
345
|
-
|
|
351
|
+
/**
|
|
352
|
+
* THE PACKAGE, NEVER THE DOT.
|
|
353
|
+
*
|
|
354
|
+
* This read `rootRaw.includes(".")` as "somebody else's namespace" - the exact
|
|
355
|
+
* rule the platform's own root reader stopped using, left behind here. So a
|
|
356
|
+
* reading that says `root: { from: "Collapsible.Root" }` stamped a TAG where a
|
|
357
|
+
* package belongs, and since this field wins over `rootTag` downstream, ten of
|
|
358
|
+
* their thirty-five components were badged `3RD-PARTY · COLLAPSIBLE.ROOT` while
|
|
359
|
+
* the census sat on the answer: `@base-ui/react/collapsible`, headless, ours to
|
|
360
|
+
* draw (dono, 03/08).
|
|
361
|
+
*
|
|
362
|
+
* The sketch stamps the package on node 0, so the answer is one lookup away and
|
|
363
|
+
* a caller that spells a tag no longer decides.
|
|
364
|
+
*/
|
|
365
|
+
const sent = String(rootObj?.from ?? "").trim();
|
|
366
|
+
const looksLikeTag = sent !== "" && /^[A-Z]/.test(sent);
|
|
367
|
+
const rootFrom = looksLikeTag ? (rootPackage ?? "") : sent;
|
|
346
368
|
if (rootFrom) {
|
|
347
369
|
rootOut = { from: rootFrom, ...paints };
|
|
348
370
|
}
|
|
349
371
|
else if (rootRaw && /^[A-Z]/.test(rootRaw)) {
|
|
350
|
-
if (rootRaw.includes(".")) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
372
|
+
if (!rootRaw.includes(".")) {
|
|
373
|
+
/**
|
|
374
|
+
* A BARE CAPITALISED NAME goes through the crosswalk, and kebab-casing is the
|
|
375
|
+
* fallback for a system we generated ourselves - the platform then checks the
|
|
376
|
+
* slug is a component it actually has, so a wrong guess degrades to nothing
|
|
377
|
+
* rather than to a hole.
|
|
378
|
+
*/
|
|
354
379
|
const slug = resolve?.(rootRaw) ?? safePartName(rootRaw);
|
|
355
380
|
if (slug)
|
|
356
381
|
rootOut = { ref: slug, name: rootRaw, ...paints };
|
|
357
382
|
}
|
|
383
|
+
else if (rootPackage) {
|
|
384
|
+
rootOut = { from: rootPackage, ...paints };
|
|
385
|
+
}
|
|
358
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* A NAMESPACED NAME WITH NO PACKAGE ANYWHERE SAYS NOTHING.
|
|
389
|
+
*
|
|
390
|
+
* The dot is not being read as "somebody else's" here - that judgement belongs to
|
|
391
|
+
* the package and nothing else. It is read as what it structurally is: a member of
|
|
392
|
+
* a namespace, which cannot be one of their own top-level components, so the
|
|
393
|
+
* crosswalk has nothing to match and kebab-casing `BaseDialog.Root` into
|
|
394
|
+
* `base-dialog-root` would invent a component nobody has. Silence is the honest
|
|
395
|
+
* answer, and the package - when the sketch stamped one - is the loud one.
|
|
396
|
+
*/
|
|
359
397
|
if (coerced > 0) {
|
|
360
398
|
notes.push(`${coerced} node${coerced === 1 ? "" : "s"} named a form that does not exist - kept, drawn as what ${coerced === 1 ? "it holds" : "they hold"}`);
|
|
361
399
|
}
|
package/dist/commands/import.js
CHANGED
|
@@ -1774,13 +1774,40 @@ async function resolveReadParts(census, root) {
|
|
|
1774
1774
|
...(libraries.length > 0 ? { external: libraries } : {}),
|
|
1775
1775
|
...(resolved.root ? { root: resolved.root } : {}),
|
|
1776
1776
|
};
|
|
1777
|
+
/**
|
|
1778
|
+
* ONE READING WRITES ONE GENERATION.
|
|
1779
|
+
*
|
|
1780
|
+
* The tree was REPLACED and the parts were MERGED, so every pass left its part
|
|
1781
|
+
* names behind under the next pass's tree. Two commands is all it takes: `--dry`
|
|
1782
|
+
* derives an anatomy and writes `box`, `box-2`, `text`; the send resolves the
|
|
1783
|
+
* reader's own and writes `trigger`, `panel`, `status-dot` - and the first
|
|
1784
|
+
* generation stays, styled and referenced by nothing.
|
|
1785
|
+
*
|
|
1786
|
+
* Measured on the owner's census (dono, 03/08): 143 orphan parts carrying real
|
|
1787
|
+
* declarations across 26 of 36 components - `CollapsibleCard` alone had 26 parts
|
|
1788
|
+
* and 13 in its tree, which is the "11 parts carry 33 declarations the anatomy
|
|
1789
|
+
* never puts on an element" the Canvas reports. The banner was right and the
|
|
1790
|
+
* cause was ours.
|
|
1791
|
+
*
|
|
1792
|
+
* A part from an EARLIER source survives only if the new tree names it - which
|
|
1793
|
+
* keeps a CSS-module part the anatomy wears, and drops the stale generation.
|
|
1794
|
+
*/
|
|
1795
|
+
const worn = new Set();
|
|
1796
|
+
const collect = (nodes) => {
|
|
1797
|
+
for (const node of nodes) {
|
|
1798
|
+
if (node.part)
|
|
1799
|
+
worn.add(node.part);
|
|
1800
|
+
if (Array.isArray(node.children))
|
|
1801
|
+
collect(node.children);
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
collect(resolved.tree);
|
|
1805
|
+
const inherited = Object.fromEntries(Object.entries(look?.parts ?? {}).filter(([part]) => worn.has(part)));
|
|
1777
1806
|
looks[component] = look
|
|
1778
1807
|
? {
|
|
1779
1808
|
...look,
|
|
1780
1809
|
...carried,
|
|
1781
|
-
...(hasParts
|
|
1782
|
-
? { parts: { ...(look.parts ?? {}), ...resolved.parts } }
|
|
1783
|
-
: {}),
|
|
1810
|
+
...(hasParts ? { parts: { ...inherited, ...resolved.parts } } : {}),
|
|
1784
1811
|
}
|
|
1785
1812
|
: {
|
|
1786
1813
|
base: {},
|
package/dist/commands/mcp.js
CHANGED
|
@@ -467,6 +467,14 @@ async function resolveAtNodes(root, entries) {
|
|
|
467
467
|
* this sends the evidence rather than a second implementation of it.
|
|
468
468
|
*/
|
|
469
469
|
...(look?.rootTag ? { rootTag: look.rootTag } : {}),
|
|
470
|
+
/**
|
|
471
|
+
* HOW LONG THE SKETCH IS, so the far side can tell an `at` that names a
|
|
472
|
+
* node from an `at` that names nothing. A reading points at the sketch by
|
|
473
|
+
* INDEX - that is what keeps a class string from being retyped - and an
|
|
474
|
+
* index past the end resolves to no classes at all, which is the silent
|
|
475
|
+
* version of sending none (dono, 02/08).
|
|
476
|
+
*/
|
|
477
|
+
...(sketch ? { sketchNodes: sketch.length } : {}),
|
|
470
478
|
}
|
|
471
479
|
: recipe0;
|
|
472
480
|
entry = { ...entry, recipe: withLook };
|
|
@@ -354,6 +354,9 @@ const STATE_SPELLING = {
|
|
|
354
354
|
"read-only": "readOnly",
|
|
355
355
|
required: "required",
|
|
356
356
|
placeholder: "placeholder",
|
|
357
|
+
/** Pseudo-ELEMENTS, which the contract spells like any other selector key. */
|
|
358
|
+
after: "after",
|
|
359
|
+
before: "before",
|
|
357
360
|
highlighted: "highlighted",
|
|
358
361
|
even: "even",
|
|
359
362
|
odd: "odd",
|
|
@@ -1004,6 +1007,78 @@ function composeGradient(utilities, declared) {
|
|
|
1004
1007
|
broken,
|
|
1005
1008
|
};
|
|
1006
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* WHICH HALF OF THE TRANSCRIPTION A MODIFIER LIST BELONGS TO.
|
|
1012
|
+
*
|
|
1013
|
+
* Extracted because a GRADIENT needs the same answer: it is three utilities that
|
|
1014
|
+
* only mean something together, so it is composed per modifier group and then has
|
|
1015
|
+
* to land in the slot that group shares. Two copies of this routing would be two
|
|
1016
|
+
* answers to "where does `dark:after:` go", and the day they differ the reader is
|
|
1017
|
+
* told a decision arrived somewhere it did not.
|
|
1018
|
+
*
|
|
1019
|
+
* MUTATES `out`: a layer or a state block is created on demand, so only call it
|
|
1020
|
+
* once there is something to write.
|
|
1021
|
+
*/
|
|
1022
|
+
function slotFor(out, modifiers) {
|
|
1023
|
+
const isDark = modifiers.includes("dark");
|
|
1024
|
+
const rest = modifiers.filter((m) => m !== "dark");
|
|
1025
|
+
if (rest.length === 0) {
|
|
1026
|
+
return { target: isDark ? out.dark : out.base, unslotted: false };
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* EVERY MODIFIER GETS A SLOT, and the contract has had one all along.
|
|
1030
|
+
*
|
|
1031
|
+
* This used to route a bare state and drop everything else, on a comment
|
|
1032
|
+
* that said "a recipe has no slot for it" - true when it was written and
|
|
1033
|
+
* false since the transcription grew `layers`, which hold `scheme`,
|
|
1034
|
+
* `state`, `within` and `at` TOGETHER. Measured on the owner's library:
|
|
1035
|
+
* 29 real decisions dropped on that stale line, most of them a `dark:`
|
|
1036
|
+
* over a state (not-expressed.md, dono, 01/08).
|
|
1037
|
+
*
|
|
1038
|
+
* A recipe that reconstructs the component must carry them, so the router
|
|
1039
|
+
* reads the whole modifier list and only gives up on a modifier that
|
|
1040
|
+
* genuinely has no home - and then it SAYS so.
|
|
1041
|
+
*/
|
|
1042
|
+
const when = {};
|
|
1043
|
+
let at;
|
|
1044
|
+
let unslotted = false;
|
|
1045
|
+
for (const mod of rest) {
|
|
1046
|
+
/** `group-hover:`, `group-data-[checked]:` - the PARENT's state. */
|
|
1047
|
+
const group = /^group-(.+)$/.exec(mod);
|
|
1048
|
+
const bare = group ? group[1] : mod;
|
|
1049
|
+
const raw = STATE_SPELLING[bare] != null
|
|
1050
|
+
? bare
|
|
1051
|
+
: (DATA_STATE.exec(bare)?.[1] ?? null);
|
|
1052
|
+
const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
|
|
1053
|
+
if (state) {
|
|
1054
|
+
if (group)
|
|
1055
|
+
when.within = state;
|
|
1056
|
+
else
|
|
1057
|
+
when.state = state;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
/** `md:`, `lg:` - a breakpoint of theirs, by their own name. */
|
|
1061
|
+
if (BREAKPOINT.test(mod) && !at) {
|
|
1062
|
+
at = mod;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
unslotted = true;
|
|
1066
|
+
}
|
|
1067
|
+
if (unslotted)
|
|
1068
|
+
return { target: null, unslotted: true };
|
|
1069
|
+
if (isDark)
|
|
1070
|
+
when.scheme = "dark";
|
|
1071
|
+
// A bare single state with no scheme keeps its own slot: `states` is what
|
|
1072
|
+
// the compiler and every lens already read, and moving it to a layer
|
|
1073
|
+
// would be a shape change nothing asked for.
|
|
1074
|
+
if (when.state && !when.within && !when.scheme && !at && rest.length === 1) {
|
|
1075
|
+
return {
|
|
1076
|
+
target: out.states[when.state] ?? (out.states[when.state] = {}),
|
|
1077
|
+
unslotted: false,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
return { target: layerFor(out, when, at).style, unslotted: false };
|
|
1081
|
+
}
|
|
1007
1082
|
export function transcribe(classes, declared) {
|
|
1008
1083
|
const out = {
|
|
1009
1084
|
base: {},
|
|
@@ -1016,92 +1091,64 @@ export function transcribe(classes, declared) {
|
|
|
1016
1091
|
fromLiteral: 0,
|
|
1017
1092
|
};
|
|
1018
1093
|
/**
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1094
|
+
* A GRADIENT IS THREE UTILITIES, AND IT BELONGS TO THE SLOT THEY SHARE.
|
|
1095
|
+
*
|
|
1096
|
+
* `composeGradient` ran on the BARE utilities only, so a gradient under any
|
|
1097
|
+
* modifier was invisible - and the one gradient in a real 36-component library
|
|
1098
|
+
* is under two of them: a Card draws its dark border as a masked `::after`
|
|
1099
|
+
* filled with `dark:after:bg-gradient-to-b dark:after:from-electric-indigo-500
|
|
1100
|
+
* dark:after:to-vivid-pink-500`. With the mask read and the paint missing, that
|
|
1101
|
+
* edge is a shape with no colour, which is the same invisible as before
|
|
1102
|
+
* (not-expressed.md, dono, 02/08).
|
|
1103
|
+
*
|
|
1104
|
+
* Grouped by the modifier list because that is what a gradient's three parts
|
|
1105
|
+
* have in common, and routed through the same `slotFor` the loop below uses.
|
|
1023
1106
|
*/
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
.map((cls) => parseClass(cls).utility);
|
|
1027
|
-
const gradient = composeGradient(bareUtilities, declared);
|
|
1028
|
-
if (gradient.value)
|
|
1029
|
-
out.base.backgroundImage = gradient.value;
|
|
1030
|
-
for (const broke of gradient.broken)
|
|
1031
|
-
out.unreadable.push(broke);
|
|
1107
|
+
const consumedByGroup = new Set();
|
|
1108
|
+
const groups = new Map();
|
|
1032
1109
|
for (const cls of classes) {
|
|
1033
1110
|
const { modifiers, utility } = parseClass(cls);
|
|
1034
|
-
|
|
1111
|
+
const key = modifiers.join(":");
|
|
1112
|
+
const entry = groups.get(key) ?? { modifiers, utilities: [] };
|
|
1113
|
+
entry.utilities.push(utility);
|
|
1114
|
+
groups.set(key, entry);
|
|
1115
|
+
}
|
|
1116
|
+
for (const group of groups.values()) {
|
|
1117
|
+
const gradient = composeGradient(group.utilities, declared);
|
|
1118
|
+
if (gradient.consumed.size === 0)
|
|
1119
|
+
continue;
|
|
1120
|
+
const prefix = group.modifiers.length
|
|
1121
|
+
? `${group.modifiers.join(":")}:`
|
|
1122
|
+
: "";
|
|
1123
|
+
for (const utility of gradient.consumed)
|
|
1124
|
+
consumedByGroup.add(`${prefix}${utility}`);
|
|
1125
|
+
if (gradient.value) {
|
|
1126
|
+
// Slotted only now: `slotFor` creates the layer it answers with, and an
|
|
1127
|
+
// empty layer is a shape the lint rejects on the way out.
|
|
1128
|
+
const { target, unslotted } = slotFor(out, group.modifiers);
|
|
1129
|
+
if (unslotted || !target) {
|
|
1130
|
+
for (const utility of gradient.consumed)
|
|
1131
|
+
out.skipped.push(`${prefix}${utility}`);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
target.backgroundImage = gradient.value;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
for (const broke of gradient.broken) {
|
|
1138
|
+
const at = `${prefix}${broke}`;
|
|
1139
|
+
if (!out.unreadable.includes(at))
|
|
1140
|
+
out.unreadable.push(at);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
for (const cls of classes) {
|
|
1144
|
+
const { modifiers, utility } = parseClass(cls);
|
|
1145
|
+
if (consumedByGroup.has(cls))
|
|
1035
1146
|
continue;
|
|
1036
|
-
const isDark = modifiers.includes("dark");
|
|
1037
|
-
const rest = modifiers.filter((m) => m !== "dark");
|
|
1038
1147
|
// THE SLOT IS DECIDED BEFORE THE VALUE IS READ. Resolving first meant a
|
|
1039
1148
|
// `dark:hover:` whose colour happened to be undeclared vanished in silence
|
|
1040
1149
|
// rather than being reported as a look we have nowhere to put - two
|
|
1041
1150
|
// different problems, and only one of them is theirs to hear about.
|
|
1042
|
-
|
|
1043
|
-
let unslotted = false;
|
|
1044
|
-
if (rest.length === 0) {
|
|
1045
|
-
target = isDark ? out.dark : out.base;
|
|
1046
|
-
}
|
|
1047
|
-
else {
|
|
1048
|
-
/**
|
|
1049
|
-
* EVERY MODIFIER GETS A SLOT, and the contract has had one all along.
|
|
1050
|
-
*
|
|
1051
|
-
* This used to route a bare state and drop everything else, on a comment
|
|
1052
|
-
* that said "a recipe has no slot for it" - true when it was written and
|
|
1053
|
-
* false since the transcription grew `layers`, which hold `scheme`,
|
|
1054
|
-
* `state`, `within` and `at` TOGETHER. Measured on the owner's library:
|
|
1055
|
-
* 29 real decisions dropped on that stale line, most of them a `dark:`
|
|
1056
|
-
* over a state (not-expressed.md, dono, 01/08).
|
|
1057
|
-
*
|
|
1058
|
-
* A recipe that reconstructs the component must carry them, so the router
|
|
1059
|
-
* reads the whole modifier list and only gives up on a modifier that
|
|
1060
|
-
* genuinely has no home - and then it SAYS so.
|
|
1061
|
-
*/
|
|
1062
|
-
const when = {};
|
|
1063
|
-
let at;
|
|
1064
|
-
for (const mod of rest) {
|
|
1065
|
-
/** `group-hover:`, `group-data-[checked]:` - the PARENT's state. */
|
|
1066
|
-
const group = /^group-(.+)$/.exec(mod);
|
|
1067
|
-
const bare = group ? group[1] : mod;
|
|
1068
|
-
const raw = STATE_SPELLING[bare] != null
|
|
1069
|
-
? bare
|
|
1070
|
-
: (DATA_STATE.exec(bare)?.[1] ?? null);
|
|
1071
|
-
const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
|
|
1072
|
-
if (state) {
|
|
1073
|
-
if (group)
|
|
1074
|
-
when.within = state;
|
|
1075
|
-
else
|
|
1076
|
-
when.state = state;
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
/** `md:`, `lg:` - a breakpoint of theirs, by their own name. */
|
|
1080
|
-
if (BREAKPOINT.test(mod) && !at) {
|
|
1081
|
-
at = mod;
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
|
-
unslotted = true;
|
|
1085
|
-
}
|
|
1086
|
-
if (!unslotted) {
|
|
1087
|
-
if (isDark)
|
|
1088
|
-
when.scheme = "dark";
|
|
1089
|
-
// A bare single state with no scheme keeps its own slot: `states` is what
|
|
1090
|
-
// the compiler and every lens already read, and moving it to a layer
|
|
1091
|
-
// would be a shape change nothing asked for.
|
|
1092
|
-
if (when.state &&
|
|
1093
|
-
!when.within &&
|
|
1094
|
-
!when.scheme &&
|
|
1095
|
-
!at &&
|
|
1096
|
-
rest.length === 1) {
|
|
1097
|
-
target = out.states[when.state] ?? (out.states[when.state] = {});
|
|
1098
|
-
}
|
|
1099
|
-
else {
|
|
1100
|
-
const layer = layerFor(out, when, at);
|
|
1101
|
-
target = layer.style;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1151
|
+
const { target, unslotted } = slotFor(out, modifiers);
|
|
1105
1152
|
const decl = readUtility(utility, declared);
|
|
1106
1153
|
if (unslotted) {
|
|
1107
1154
|
// Only worth saying when it IS a design value; an unslotted `sm:flex` is
|
package/package.json
CHANGED