synthesisui 0.16.111 → 0.16.113

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.
@@ -308,12 +308,30 @@ versionOf) {
308
308
  */
309
309
  const local = capitalised && !node.from;
310
310
  /**
311
- * A WRAPPER THAT RENDERS NOTHING passes its children up in its own place: a
312
- * portal teleports and a provider is context. Keeping it would draw a box
313
- * that does not exist in the client's output.
311
+ * A WRAPPER THAT RENDERS NOTHING DRAWS NOTHING AND IS STILL REQUIRED.
312
+ *
313
+ * A portal teleports and a provider is context, so keeping it as a box would draw
314
+ * something the client's output does not have. But `Collapsible.Panel`,
315
+ * `Dialog.Portal` and `Select.Positioner` are what make the component WORK, and the
316
+ * usage snippet was leaving them out: 27 of them across 8 of their components, so
317
+ * somebody pasting the snippet got a card that never opens (dono, 03/08).
318
+ *
319
+ * So it keeps its place in the tree with no `name` - no part, no styles, nothing for
320
+ * the Canvas to draw, and an element the snippet emits. A motion wrapper stays
321
+ * flattened: it is animation, and its element is genuinely absent from the output.
314
322
  */
315
- if ((kind === "motion" || kind === "headless") &&
316
- rendersNothing(tag, classes)) {
323
+ if (kind === "headless" && rendersNothing(tag, classes)) {
324
+ const held = children.flatMap((child) => build(child, depth + 1));
325
+ return [
326
+ {
327
+ as: held.length > 0 ? "stack" : "text",
328
+ from: String(node.from ?? tag),
329
+ refName: tag,
330
+ ...(held.length > 0 ? { children: held } : {}),
331
+ },
332
+ ];
333
+ }
334
+ if (kind === "motion" && rendersNothing(tag, classes)) {
317
335
  return children.flatMap((child) => build(child, depth));
318
336
  }
319
337
  // `<AnimatePresence>` and friends: no element of their own.
@@ -265,6 +265,25 @@ sketch) {
265
265
  continue;
266
266
  }
267
267
  const node_ = { as };
268
+ /**
269
+ * THE GUARD TRAVELS WITH THE NODE, off the sketch it points at - so neither a
270
+ * reading nor the deriver has to carry it, and both get it.
271
+ */
272
+ if (typeof node.at === "number") {
273
+ const measured = sketch?.[node.at];
274
+ const when = node.when ?? measured?.when;
275
+ if (when)
276
+ node_.when = when;
277
+ const atRest = node.atRest ?? measured?.atRest;
278
+ if (atRest === false)
279
+ node_.atRest = false;
280
+ }
281
+ else {
282
+ if (node.when)
283
+ node_.when = node.when;
284
+ if (node.atRest === false)
285
+ node_.atRest = false;
286
+ }
268
287
  /**
269
288
  * A PART THAT IS A LIBRARY ELEMENT CARRIES BOTH FACTS - see `AnatomyRead.from`.
270
289
  * Inert for the renderer, which dispatches on `as`; read by the usage snippet,
@@ -395,7 +414,34 @@ sketch) {
395
414
  ? (frontierClasses(rootObj, sketch).classes ??
396
415
  "")
397
416
  : "";
398
- const paints = rootPainted ? { classes: rootPainted } : {};
417
+ /**
418
+ * THE OVERRIDE TRAVELS AS DECLARATIONS, AND ONLY WHEN IT IS AN OVERRIDE.
419
+ *
420
+ * Two different things were sharing one field, and both were broken by it.
421
+ *
422
+ * When the root is a FRONTIER of theirs, the class string at the call site is what
423
+ * the PARENT paints on it - `MetricCard` returns `<Card className="flex-1
424
+ * dark:bg-darkgray-300">` - and this is the whole reason the field exists. It was
425
+ * sent as the class NAMES, so the platform pasted `dark:bg-darkgray-300` onto its own
426
+ * element and the browser resolved it against OUR Tailwind, which has no such colour:
427
+ * the override did nothing, on 5 of 5 of their components (dono, 03/08). Transcribed,
428
+ * it is a declaration that applies anywhere.
429
+ *
430
+ * When the root is one we DRAW - a headless primitive, a plain element - the classes
431
+ * at that index are the component's OWN look and are already in `base`. Sending them
432
+ * again put their raw `bg-white` on our element, in a layer that outranks the compiled
433
+ * recipe, and two of their cards previewed white in dark mode over a recipe that said
434
+ * otherwise. So a drawn root sends nothing here at all.
435
+ */
436
+ const rootStyle = rootPainted
437
+ ? transcribe(rootPainted.split(/\s+/).filter(Boolean), declared)
438
+ : null;
439
+ const paints = rootStyle && Object.keys(rootStyle.base).length > 0
440
+ ? { style: rootStyle.base }
441
+ : {};
442
+ const paintsDark = rootStyle && Object.keys(rootStyle.dark).length > 0
443
+ ? { dark: rootStyle.dark }
444
+ : {};
399
445
  /**
400
446
  * A LIBRARY ROOT WHOSE NAME IS NOT DOTTED.
401
447
  *
@@ -427,7 +473,8 @@ sketch) {
427
473
  const looksLikeTag = sent !== "" && /^[A-Z]/.test(sent);
428
474
  const rootFrom = looksLikeTag ? (rootPackage ?? "") : sent;
429
475
  if (rootFrom) {
430
- rootOut = { from: rootFrom, ...paints };
476
+ // A root we draw carries no override: its classes are the component's own.
477
+ rootOut = { from: rootFrom };
431
478
  }
432
479
  else if (rootRaw && /^[A-Z]/.test(rootRaw)) {
433
480
  if (!rootRaw.includes(".")) {
@@ -439,10 +486,10 @@ sketch) {
439
486
  */
440
487
  const slug = resolve?.(rootRaw) ?? safePartName(rootRaw);
441
488
  if (slug)
442
- rootOut = { ref: slug, name: rootRaw, ...paints };
489
+ rootOut = { ref: slug, name: rootRaw, ...paints, ...paintsDark };
443
490
  }
444
491
  else if (rootPackage) {
445
- rootOut = { from: rootPackage, ...paints };
492
+ rootOut = { from: rootPackage };
446
493
  }
447
494
  }
448
495
  /**
@@ -24,6 +24,7 @@ import { sketchOf } from "../doctor/sketch.js";
24
24
  import { buildTable } from "../doctor/tokens.js";
25
25
  import { readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
26
26
  import { transcribeVariants } from "../doctor/variant-read.js";
27
+ import { withLibraryStructure } from "../library-structure.js";
27
28
  import { body, paint, section } from "../output.js";
28
29
  import { phase, startProgress } from "../progress.js";
29
30
  import { detectStack, resolveDeps } from "../stack.js";
@@ -1793,8 +1794,22 @@ async function resolveReadParts(census, root) {
1793
1794
  for (const note of derived.notes)
1794
1795
  notes.add(note);
1795
1796
  }
1796
- const resolved = Array.isArray(anatomy) && anatomy.length > 0
1797
- ? resolveAnatomy(anatomy, declared, deps, resolveRef, entry?.root,
1797
+ /**
1798
+ * THE LIBRARY'S OWN FRAME, PUT BACK BEFORE ANYTHING IS RESOLVED.
1799
+ *
1800
+ * A reading names what carries appearance; a headless library's `Portal`,
1801
+ * `Positioner` and `Panel` carry none and are what make the component work. 27 of
1802
+ * them across 8 of their components were named by nobody, so the usage snippet
1803
+ * handed people a card that never opens (dono, 03/08). See `withLibraryStructure` -
1804
+ * it is a lookup in the sketch, not a guess, and it is idempotent.
1805
+ */
1806
+ const framed = Array.isArray(anatomy) && anatomy.length > 0
1807
+ ? withLibraryStructure(anatomy, looks[component]?.sketch, typeof entry?.root?.at === "number"
1808
+ ? (entry?.root).at
1809
+ : undefined)
1810
+ : anatomy;
1811
+ const resolved = Array.isArray(framed) && framed.length > 0
1812
+ ? resolveAnatomy(framed, declared, deps, resolveRef, entry?.root,
1798
1813
  // The component's own sketch, so a node naming itself by index reads
1799
1814
  // the exact class string the census measured (dono, 01/08).
1800
1815
  looks[component]?.sketch)
@@ -1851,20 +1866,34 @@ async function resolveReadParts(census, root) {
1851
1866
  const split = placeLayers((look?.layers ?? []), partAt, (look?.sketch ?? []));
1852
1867
  for (const line of split.unplaced)
1853
1868
  notes.add(line);
1854
- /** The per-part layers, merged into the parts the resolution just named. */
1869
+ /**
1870
+ * REPLACED, NOT APPENDED - because this runs more than once.
1871
+ *
1872
+ * A measuring pass places these onto the DERIVED parts (`box-2`); the send pass
1873
+ * places them onto the AUTHORED ones (`status-dot`). Appending would accumulate a
1874
+ * generation per run, and the pruning below drops the derived names anyway.
1875
+ */
1855
1876
  for (const [part, list] of split.perPart) {
1856
1877
  const target = resolved.parts[part];
1857
1878
  if (!target || !list)
1858
1879
  continue;
1859
- target.layers = [
1860
- ...(target.layers ?? []),
1861
- ...list,
1862
- ];
1880
+ target.layers = list;
1863
1881
  }
1864
1882
  const carried = {
1865
1883
  ...(hasParts ? { parts: resolved.parts } : {}),
1866
- /** The root keeps only what is the root's - see the note above. */
1867
- ...(look?.layers ? { layers: split.root } : {}),
1884
+ /**
1885
+ * THE FULL LIST STAYS ON THE LOOK, EACH LAYER KEEPING ITS `on`.
1886
+ *
1887
+ * Writing back only the root's was destructive, and this function runs twice: the
1888
+ * measuring pass moved 8 of their status layers onto `box-2` and wrote the
1889
+ * remaining 2 back, then the send pass re-read those 2, had nothing to place, and
1890
+ * the one-generation pruning dropped `box-2` - so the four status colours of their
1891
+ * dot and label reached zero recipes, on 36 of 36 components (dono, 03/08).
1892
+ *
1893
+ * Keeping the source of truth here makes the split idempotent: every pass
1894
+ * recomputes both halves from the same list. The RECIPE builder is what leaves an
1895
+ * attributed layer off the component - see `safeLayers`.
1896
+ */
1868
1897
  ...(resolved.tree.length > 0 ? { tree: resolved.tree } : {}),
1869
1898
  ...(resolved.composes.length > 0 ? { composes: resolved.composes } : {}),
1870
1899
  ...(libraries.length > 0 ? { external: libraries } : {}),
@@ -0,0 +1,225 @@
1
+ /**
2
+ * IS THIS ELEMENT THERE WHEN NOBODY HAS TOUCHED THE COMPONENT?
3
+ *
4
+ * Their markup guards elements with conditions - `{isLoading && (…)}`,
5
+ * `{status === "success" && (…)}` - and the preview drew every one of them. So a card
6
+ * that is not loading wore a loading veil across its whole surface, and a status that is
7
+ * `waiting` showed the ping that belongs to `success` (dono, 03/08).
8
+ *
9
+ * ANSWERED FROM THEIR OWN DECLARATIONS, or not answered at all. Three facts, all
10
+ * measured in their source and none of them ours:
11
+ *
12
+ * a prop's default `{ status = "waiting" }`
13
+ * a local alias `const isLoading = status === "loading"`
14
+ * a state's first value `useState(defaultOpen)` → the default of `defaultOpen`
15
+ *
16
+ * `null` means the condition cannot be settled - `{tag && <Tag/>}` on a component whose
17
+ * `tag` has no default is optional CONTENT, and drawing it is right: 56 of their 79
18
+ * guards are that shape, and hiding them would empty the previews out. Only a condition
19
+ * that resolves to FALSE takes an element off the resting view, and the blueprint says
20
+ * every one of them out loud either way.
21
+ */
22
+ const STATE = /const\s*\[\s*([A-Za-z_$][\w$]*)\s*,[^\]]*\]\s*=\s*(?:React\.)?useState[^(]*\(\s*([^)]*?)\s*\)/g;
23
+ const ALIAS = /const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*([^;\n]+);/g;
24
+ /**
25
+ * The aliases and first-render state values a component declares about itself.
26
+ *
27
+ * A state read is honest here for one reason: its INITIAL value is what "at rest" means.
28
+ * `useState(defaultOpen)` with `defaultOpen = false` says a collapsible card starts
29
+ * closed, which is a fact about their component rather than a guess about a user.
30
+ */
31
+ /**
32
+ * EVERY DEFAULT THE DESTRUCTURING DECLARES, not only the string ones.
33
+ *
34
+ * `readDefinitionProps` reads string literals on purpose - it answers "which option is
35
+ * the resting one", and a boolean is not an option. Here a boolean is the whole answer:
36
+ * `{ defaultOpen = false }` is what says a collapsible card starts closed.
37
+ */
38
+ function paramDefaults(body) {
39
+ const open = body.indexOf("({");
40
+ if (open === -1)
41
+ return {};
42
+ let depth = 0;
43
+ let end = -1;
44
+ for (let i = open + 1; i < body.length; i++) {
45
+ const c = body[i];
46
+ if (c === "{")
47
+ depth++;
48
+ else if (c === "}") {
49
+ depth--;
50
+ if (depth === 0) {
51
+ end = i;
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ if (end === -1)
57
+ return {};
58
+ const out = {};
59
+ let depth2 = 0;
60
+ let part = "";
61
+ const take = () => {
62
+ const eq = part.indexOf("=");
63
+ if (eq > 0) {
64
+ const key = part.slice(0, eq).split(":")[0].trim();
65
+ const value = part.slice(eq + 1).trim();
66
+ if (/^[A-Za-z_$][\w$]*$/.test(key) && value)
67
+ out[key] = value;
68
+ }
69
+ part = "";
70
+ };
71
+ for (const c of body.slice(open + 2, end)) {
72
+ if ("([{".includes(c))
73
+ depth2++;
74
+ else if (")]}".includes(c))
75
+ depth2--;
76
+ if (c === "," && depth2 === 0)
77
+ take();
78
+ else
79
+ part += c;
80
+ }
81
+ take();
82
+ return out;
83
+ }
84
+ export function restingFacts(body, defaults) {
85
+ const aliases = {};
86
+ ALIAS.lastIndex = 0;
87
+ for (const m of body.matchAll(ALIAS)) {
88
+ const value = m[2].trim();
89
+ // A function or a component is not a condition; only value-shaped reads help.
90
+ if (/=>|\bfunction\b/.test(value))
91
+ continue;
92
+ if (!aliases[m[1]])
93
+ aliases[m[1]] = value;
94
+ }
95
+ STATE.lastIndex = 0;
96
+ for (const m of body.matchAll(STATE)) {
97
+ if (!aliases[m[1]])
98
+ aliases[m[1]] = m[2].trim() || "undefined";
99
+ }
100
+ /**
101
+ * THE DESTRUCTURING WINS, because it kept the quotes.
102
+ *
103
+ * `readDefinitionProps` hands back `waiting` already unquoted, and an unquoted word is
104
+ * indistinguishable from a reference to something else - so `status === "success"` went
105
+ * unsettled while the answer sat in the source. Read verbatim, `"waiting"` is a value
106
+ * and `defaultOpen` is a name, and the shape says which.
107
+ */
108
+ return { defaults: { ...defaults, ...paramDefaults(body) }, aliases };
109
+ }
110
+ const LITERAL = /^(?:"([^"]*)"|'([^']*)'|`([^`]*)`)$/;
111
+ /** A value their code wrote, as the string a comparison would see. */
112
+ function literalOf(text) {
113
+ const t = text.trim();
114
+ const m = LITERAL.exec(t);
115
+ if (m)
116
+ return m[1] ?? m[2] ?? m[3] ?? "";
117
+ if (t === "true" || t === "false" || t === "undefined" || t === "null")
118
+ return t;
119
+ if (/^-?\d+(\.\d+)?$/.test(t))
120
+ return t;
121
+ return null;
122
+ }
123
+ /** Split on a top-level operator, so `a && (b || c)` reads as two halves. */
124
+ function splitTop(text, op) {
125
+ let depth = 0;
126
+ for (let i = 0; i < text.length - 1; i++) {
127
+ const c = text[i];
128
+ if (c === "(" || c === "[" || c === "{")
129
+ depth++;
130
+ else if (c === ")" || c === "]" || c === "}")
131
+ depth--;
132
+ else if (depth === 0 && text.startsWith(op, i))
133
+ return [text.slice(0, i), text.slice(i + 2)];
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * True, false, or `null` when their own declarations do not settle it.
139
+ *
140
+ * `seen` guards a chain that refers back to itself - `const a = b; const b = a` is not
141
+ * something to crash on.
142
+ */
143
+ export function holdsAtRest(condition, facts, seen = new Set()) {
144
+ const text = condition.trim();
145
+ if (!text)
146
+ return null;
147
+ if (text.startsWith("(") && text.endsWith(")")) {
148
+ const inner = text.slice(1, -1);
149
+ if (splitTop(inner, "&&") || splitTop(inner, "||") || !/[()]/.test(inner))
150
+ return holdsAtRest(inner, facts, seen);
151
+ }
152
+ const and = splitTop(text, "&&");
153
+ if (and) {
154
+ const [a, b] = [
155
+ holdsAtRest(and[0], facts, seen),
156
+ holdsAtRest(and[1], facts, seen),
157
+ ];
158
+ if (a === false || b === false)
159
+ return false;
160
+ return a === true && b === true ? true : null;
161
+ }
162
+ const or = splitTop(text, "||");
163
+ if (or) {
164
+ const [a, b] = [
165
+ holdsAtRest(or[0], facts, seen),
166
+ holdsAtRest(or[1], facts, seen),
167
+ ];
168
+ if (a === true || b === true)
169
+ return true;
170
+ return a === false && b === false ? false : null;
171
+ }
172
+ if (text.startsWith("!")) {
173
+ const inner = holdsAtRest(text.slice(1), facts, seen);
174
+ return inner === null ? null : !inner;
175
+ }
176
+ const cmp = /^(.+?)\s*(===|!==|==|!=)\s*(.+)$/.exec(text);
177
+ if (cmp) {
178
+ const left = restingValue(cmp[1], facts, seen);
179
+ const right = literalOf(cmp[3]) ?? restingValue(cmp[3], facts, seen);
180
+ if (left === null || right === null)
181
+ return null;
182
+ const same = left === right;
183
+ return cmp[2].startsWith("!") ? !same : same;
184
+ }
185
+ const value = restingValue(text, facts, seen);
186
+ if (value === null)
187
+ return null;
188
+ return (value !== "false" &&
189
+ value !== "undefined" &&
190
+ value !== "null" &&
191
+ value !== "");
192
+ }
193
+ /** What a name holds at rest, as a literal string, or null when nothing says. */
194
+ function restingValue(name, facts, seen) {
195
+ const key = name.trim();
196
+ if (!/^[A-Za-z_$][\w$]*$/.test(key) || seen.has(key))
197
+ return null;
198
+ const direct = facts.defaults[key];
199
+ if (direct != null) {
200
+ const asLiteral = literalOf(direct);
201
+ if (asLiteral != null)
202
+ return asLiteral;
203
+ /**
204
+ * `readDefinitionProps` hands back `waiting`, already unquoted, while the
205
+ * destructuring reads `defaultOpen` - a NAME to follow. One is a value and the other
206
+ * is a reference, and the shape says which.
207
+ */
208
+ if (/^[A-Za-z_$][\w$]*$/.test(direct.trim()))
209
+ return restingValue(direct, facts, new Set([...seen, key]));
210
+ return direct.trim();
211
+ }
212
+ const alias = facts.aliases[key];
213
+ if (alias == null)
214
+ return null;
215
+ const next = new Set([...seen, key]);
216
+ const asLiteral = literalOf(alias);
217
+ if (asLiteral != null)
218
+ return asLiteral;
219
+ /**
220
+ * The alias is an expression - `const isLoading = status === "loading"`. Settling it is
221
+ * the same question one level down, and the answer comes back as a boolean.
222
+ */
223
+ const held = holdsAtRest(alias, facts, next);
224
+ return held === null ? null : String(held);
225
+ }
@@ -18,8 +18,10 @@
18
18
  * GENERALIZED BY CONSTRUCTION: it reads JSX shape, not folder layout, so it works the
19
19
  * same in an atomic library, a feature-foldered app, or a single flat directory.
20
20
  */
21
+ import { readDefinitionProps } from "./call-sites.js";
21
22
  import { scanTags } from "./css-modules.js";
22
23
  import { importMap, packageOfTag } from "./imports.js";
24
+ import { holdsAtRest, restingFacts } from "./resting.js";
23
25
  import { definitionSpan, returnAt } from "./transcribe.js";
24
26
  /** `{children}` / `{props.children}` - the caller's content, spelled either way. */
25
27
  const CHILDREN = /\{\s*(?:props\.)?children\s*\}/;
@@ -160,6 +162,87 @@ function staticCallClasses(body) {
160
162
  * sitting directly between an open and its close - dynamic children are the caller's,
161
163
  * and the slot form already says so.
162
164
  */
165
+ /**
166
+ * THE CONDITION THAT HAS TO HOLD FOR THIS ELEMENT TO EXIST.
167
+ *
168
+ * Their code guards markup with `{isLoading && (…)}` and `{imageUrl ? <img/> : <box/>}`,
169
+ * and the sketch recorded the element without the guard - so the preview drew a loading
170
+ * veil over a card that is not loading and a status ping for a status it does not have
171
+ * (dono, 03/08). Measured on their library: 76 guards across 22 of 36 components.
172
+ *
173
+ * VERBATIM, in their own vocabulary. This is not a condition we evaluate; it is a fact
174
+ * about their markup that the screen can say out loud ("only when `isLoading`"). The
175
+ * falsy branch of a ternary reads as the negation of the same expression, which is their
176
+ * logic restated rather than anything of ours.
177
+ */
178
+ export function guardAt(source, at) {
179
+ let i = at - 1;
180
+ const skip = () => {
181
+ while (i >= 0 && /\s/.test(source[i]))
182
+ i--;
183
+ };
184
+ skip();
185
+ // `{cond && (` and `{cond ? (` both put a paren between the operator and the tag.
186
+ if (source[i] === "(") {
187
+ i--;
188
+ skip();
189
+ }
190
+ let negated = false;
191
+ if (source[i] === "&" && source[i - 1] === "&")
192
+ i -= 2;
193
+ else if (source[i] === "?")
194
+ i -= 1;
195
+ else if (source[i] === ":") {
196
+ // The ELSE half of `cond ? <a/> : <b/>` - it exists when the condition does not.
197
+ negated = true;
198
+ i -= 1;
199
+ // Walk back over the truthy branch to reach the `?`.
200
+ let depth = 0;
201
+ while (i >= 0) {
202
+ const c = source[i];
203
+ if (c === ">" || c === ")" || c === "}")
204
+ depth++;
205
+ else if (c === "<" || c === "(" || c === "{") {
206
+ if (depth === 0)
207
+ return null;
208
+ depth--;
209
+ }
210
+ else if (c === "?" && depth === 0)
211
+ break;
212
+ i--;
213
+ }
214
+ if (i < 0 || source[i] !== "?")
215
+ return null;
216
+ i -= 1;
217
+ }
218
+ else
219
+ return null;
220
+ // Back to the `{` that opened the expression, minding nesting on the way.
221
+ let depth = 0;
222
+ const end = i;
223
+ while (i >= 0) {
224
+ const c = source[i];
225
+ if (c === ")" || c === "]" || c === "}")
226
+ depth++;
227
+ else if (c === "(" || c === "[") {
228
+ if (depth === 0)
229
+ return null;
230
+ depth--;
231
+ }
232
+ else if (c === "{") {
233
+ if (depth === 0)
234
+ break;
235
+ depth--;
236
+ }
237
+ i--;
238
+ }
239
+ if (i < 0 || source[i] !== "{")
240
+ return null;
241
+ const cond = source.slice(i + 1, end + 1).trim();
242
+ if (!cond || cond.length > 90 || cond.includes("\n"))
243
+ return null;
244
+ return negated ? `!(${cond})` : cond;
245
+ }
163
246
  export function sketchOf(source, name) {
164
247
  /**
165
248
  * READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
@@ -189,6 +272,14 @@ export function sketchOf(source, name) {
189
272
  */
190
273
  const ownReturn = returnAt(source, name);
191
274
  const rootAt = ownReturn === -1 ? -1 : source.indexOf("<", ownReturn);
275
+ /**
276
+ * WHAT THE COMPONENT DECLARES ABOUT ITSELF, read once for the whole sketch.
277
+ *
278
+ * Against the SLICED source, so a file that declares two components does not lend one
279
+ * of them the other's defaults - `RadioCard.tsx` declares `RadioCard` and
280
+ * `RadioCardGroup`, and that is the same slice the node indices are numbered against.
281
+ */
282
+ const facts = restingFacts(source, readDefinitionProps(name ?? "", source).defaults);
192
283
  const events = scanTags(source);
193
284
  const out = [];
194
285
  let depth = 0;
@@ -210,6 +301,14 @@ export function sketchOf(source, name) {
210
301
  if (pkg)
211
302
  node.from = pkg;
212
303
  }
304
+ // What has to be true for this element to exist at all - see `guardAt`.
305
+ const guard = guardAt(source, event.at);
306
+ if (guard) {
307
+ node.when = guard;
308
+ // And whether their own defaults settle it - see `holdsAtRest`.
309
+ if (holdsAtRest(guard, facts) === false)
310
+ node.atRest = false;
311
+ }
213
312
  const cls = CLASS_ATTR.exec(event.body);
214
313
  if (cls?.[1].trim())
215
314
  node.classes = cls[1].trim();
@@ -401,10 +401,44 @@ export function parseClass(cls) {
401
401
  * `declared` maps a token NAME to its value, so a resolved colour can be
402
402
  * reported in their vocabulary rather than as a hex we looked up.
403
403
  */
404
+ /** A hex, or a ref into their colour scale - the two shapes an alpha can wrap. */
405
+ const COLOUR_VALUE = /^(#[0-9a-f]{3,8}|\{color\.[^}]+\})$/i;
406
+ /**
407
+ * `#ffffff` + `50` → `color-mix(in srgb, #ffffff 50%, transparent)`.
408
+ *
409
+ * Left alone when the value is not a colour (`shadow-lg/50` is not one) or when mixing
410
+ * would say nothing (`transparent`, `currentColor`).
411
+ */
412
+ function withAlpha(d, alpha) {
413
+ if (!alpha || !/^\d{1,3}$/.test(alpha))
414
+ return d;
415
+ const pct = Number(alpha);
416
+ if (pct >= 100 || pct < 0)
417
+ return d;
418
+ if (!COLOUR_VALUE.test(d.value.trim()))
419
+ return d;
420
+ return { ...d, value: `color-mix(in srgb, ${d.value} ${pct}%, transparent)` };
421
+ }
404
422
  export function readUtility(utility, declared) {
405
- // An opacity modifier changes the value, not the property, and honouring it
406
- // would mean computing a colour they never wrote. The property still belongs
407
- // in the recipe, so the base colour travels and the alpha does not.
423
+ const read = readUtilityCore(utility, declared);
424
+ return read ? withAlpha(read, utility.split("/")[1]) : read;
425
+ }
426
+ function readUtilityCore(utility, declared) {
427
+ /**
428
+ * THE ALPHA TRAVELS, and it took `color-mix` to make that honest.
429
+ *
430
+ * This dropped the modifier on purpose: honouring `bg-white/50` used to mean computing
431
+ * a hex they never wrote, and inventing values is the one thing this reader may not
432
+ * do. But `color-mix(in srgb, X 50%, transparent)` computes nothing - the browser does,
433
+ * at paint time - and a token REF stays a ref inside it, so Token Discipline still sees
434
+ * the token and a re-theme still moves it.
435
+ *
436
+ * Dropping it was not neutral. Their loading veil is `bg-white/50 absolute inset-0`:
437
+ * as an opaque white it covered the whole card, which is the white box the owner kept
438
+ * asking about. Their Modal's backdrop is `bg-darkgray-900/40` - a scrim that became a
439
+ * wall. Measured on their library: 11 of 11 opacity modifiers lost their alpha
440
+ * (dono, 03/08).
441
+ */
408
442
  const [core] = utility.split("/");
409
443
  // A bare keyword carries a declaration with no scale behind it, so it is read
410
444
  // before the dash split that everything else needs.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * THE ELEMENTS THAT MAKE IT WORK, PUT BACK.
3
+ *
4
+ * A reading names the elements that carry APPEARANCE, because that is what a design
5
+ * system is for. A headless library's most important elements carry none: a
6
+ * `Dialog.Portal` teleports, a `Select.Positioner` measures, a `Collapsible.Panel` shows
7
+ * and hides. They have no class list, so nothing of ours styles them - and a reading that
8
+ * skips them is not wrong about the styling, it is just missing the frame.
9
+ *
10
+ * The usage snippet is where that costs something. Measured on the owner's library
11
+ * against the reading his own skill authored (dono, 03/08):
12
+ *
13
+ * 27 library elements with no classes, across 8 components, named by nobody
14
+ * Modal BaseDialog.Root, BaseDialog.Portal
15
+ * Select BaseSelect.Root, BaseSelect.Portal
16
+ * Tooltip BaseTooltip.Provider, .Root, .Portal, .Positioner
17
+ * TextEditor 11 (four Popovers and a Dialog)
18
+ * Menu, ColorPicker, CollapsibleCard, Toggle the rest
19
+ *
20
+ * Somebody pasting that snippet gets a card that never opens. So this puts them back,
21
+ * and it is a LOOKUP rather than a guess: the sketch already recorded every element with
22
+ * its depth and its package, so the chain between two named nodes is a fact about their
23
+ * markup. Nothing is invented - a node re-inserted here carries no name, no part and no
24
+ * styles, only the element and where it came from.
25
+ *
26
+ * A library element that DOES carry classes is left alone: it belongs to whoever named
27
+ * it, and re-inserting it would be a second opinion about a styled element.
28
+ */
29
+ import { frontierKind } from "./frontier-kind.js";
30
+ /** Index → the index of its parent, or -1. Read off `depth`, which is the nesting. */
31
+ function parents(sketch) {
32
+ const out = [];
33
+ const open = [];
34
+ for (const [i, node] of sketch.entries()) {
35
+ while (open.length > 0 && sketch[open[open.length - 1]].depth >= node.depth)
36
+ open.pop();
37
+ out.push(open.length > 0 ? open[open.length - 1] : -1);
38
+ open.push(i);
39
+ }
40
+ return out;
41
+ }
42
+ /** Every `at` the reading already points at, at any depth. */
43
+ function claimed(read, into = new Set()) {
44
+ for (const node of read) {
45
+ if (typeof node.at === "number")
46
+ into.add(node.at);
47
+ if (node.children)
48
+ claimed(node.children, into);
49
+ }
50
+ return into;
51
+ }
52
+ /** Behaviour with no appearance: their element, no class list, nobody's part. */
53
+ function isBehaviour(node) {
54
+ if (!node)
55
+ return false;
56
+ if ((node.classes ?? "").trim())
57
+ return false;
58
+ return frontierKind(node.from) === "headless";
59
+ }
60
+ /**
61
+ * Put the missing library frame back around the nodes it wraps.
62
+ *
63
+ * `boundary` is the `at` of the nearest ancestor the reading DID name (or -1 at the top),
64
+ * so the chain walked is exactly what sits between one named element and the next.
65
+ */
66
+ function wrap(read, sketch, parentOf, named, boundary,
67
+ /**
68
+ * The library elements already standing on this path, by their own spelling.
69
+ *
70
+ * A restored wrapper carries no index - it is not a node of the reading - so the
71
+ * `named` set cannot see it, and without this a second run wrapped the same portal
72
+ * again. That matters because the derived anatomy already keeps these, and both
73
+ * anatomies go through here.
74
+ */
75
+ wrapped) {
76
+ /** The missing library ancestors of `at`, outermost first. */
77
+ const chain = (at) => {
78
+ const out = [];
79
+ for (let p = parentOf[at]; p >= 0 && p !== boundary; p = parentOf[p]) {
80
+ if (named.has(p) || wrapped.has(sketch[p].tag))
81
+ continue;
82
+ if (isBehaviour(sketch[p]))
83
+ out.unshift(p);
84
+ }
85
+ return out;
86
+ };
87
+ const deep = read.map((node) => {
88
+ if (!node.children || node.children.length === 0)
89
+ return node;
90
+ const inside = node.refName ? new Set([...wrapped, node.refName]) : wrapped;
91
+ const inner = typeof node.at === "number"
92
+ ? wrap(node.children, sketch, parentOf, named, node.at, inside)
93
+ : wrap(node.children, sketch, parentOf, named, boundary, inside);
94
+ return { ...node, children: inner };
95
+ });
96
+ /**
97
+ * SIBLINGS THAT SHARE A WRAPPER SHARE ONE. Their Select puts a trigger and a popup
98
+ * inside one `BaseSelect.Root`; two wrappers would say their code has two.
99
+ */
100
+ const out = [];
101
+ let run = null;
102
+ const flush = () => {
103
+ if (!run)
104
+ return;
105
+ let built = run.nodes;
106
+ for (const at of [...run.keys].reverse()) {
107
+ const node = sketch[at];
108
+ built = [
109
+ {
110
+ as: "stack",
111
+ from: String(node.from ?? node.tag),
112
+ refName: node.tag,
113
+ children: built,
114
+ },
115
+ ];
116
+ }
117
+ out.push(...built);
118
+ run = null;
119
+ };
120
+ for (const node of deep) {
121
+ const keys = typeof node.at === "number" ? chain(node.at) : [];
122
+ if (keys.length === 0) {
123
+ flush();
124
+ out.push(node);
125
+ continue;
126
+ }
127
+ if (run && run.keys.join(",") === keys.join(","))
128
+ run.nodes.push(node);
129
+ else {
130
+ flush();
131
+ run = { keys, nodes: [node] };
132
+ }
133
+ }
134
+ flush();
135
+ return out;
136
+ }
137
+ /**
138
+ * The reading with the library's own structure restored around it.
139
+ *
140
+ * Idempotent: an element the reading already names is never re-inserted, so running this
141
+ * over a derived anatomy - which keeps them already - changes nothing.
142
+ */
143
+ export function withLibraryStructure(read, sketch,
144
+ /** The `at` the reading gave for the element the component RETURNS, when it gave one. */
145
+ rootAt) {
146
+ if (!sketch || sketch.length === 0 || read.length === 0)
147
+ return read;
148
+ /**
149
+ * THE ELEMENT THE COMPONENT RETURNS IS THE BOUNDARY, and it is not always given.
150
+ *
151
+ * A reading may name its root by NAME alone (`"root": "BaseDialog.Root"`), and then
152
+ * the walk climbed past it and restored the root as a wrapper INSIDE the anatomy -
153
+ * so the snippet would open `<BaseDialog.Root>` twice. The sketch stamps the returned
154
+ * element, so the answer is measured either way.
155
+ */
156
+ const stamped = sketch.findIndex((node) => node.root);
157
+ const from = typeof rootAt === "number" ? rootAt : stamped >= 0 ? stamped : -1;
158
+ const named = claimed(read);
159
+ if (from >= 0)
160
+ named.add(from);
161
+ return wrap(read, sketch, parents(sketch), named, from, new Set());
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.111",
3
+ "version": "0.16.113",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {