synthesisui 0.16.110 → 0.16.112
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-from-sketch.js +40 -5
- package/dist/anatomy-read.js +43 -4
- package/dist/commands/import.js +38 -9
- package/dist/doctor/transcribe.js +37 -3
- package/dist/library-structure.js +162 -0
- package/package.json +1 -1
|
@@ -308,12 +308,30 @@ versionOf) {
|
|
|
308
308
|
*/
|
|
309
309
|
const local = capitalised && !node.from;
|
|
310
310
|
/**
|
|
311
|
-
* A WRAPPER THAT RENDERS NOTHING
|
|
312
|
-
*
|
|
313
|
-
*
|
|
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 (
|
|
316
|
-
|
|
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.
|
|
@@ -436,10 +454,27 @@ versionOf) {
|
|
|
436
454
|
},
|
|
437
455
|
];
|
|
438
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* A BOX OF OURS THAT IS SOMEBODY ELSE'S ELEMENT SAYS SO.
|
|
459
|
+
*
|
|
460
|
+
* A headless primitive is ours to draw - the classes on it are the client's - and
|
|
461
|
+
* that is why it becomes a PART rather than a frontier. But the element itself is
|
|
462
|
+
* `<Collapsible.Trigger>`, and the usage snippet was emitting a plain `<div>` with
|
|
463
|
+
* our class on it: a person following it gets the look and none of the behaviour, no
|
|
464
|
+
* `aria-expanded` and no keyboard (dono, 03/08). Measured on their library: 31 parts
|
|
465
|
+
* across 9 components are a library element, plus 11 roots.
|
|
466
|
+
*
|
|
467
|
+
* The package AND their own spelling, because the snippet needs both - the import
|
|
468
|
+
* line comes from the first and the tag from the second.
|
|
469
|
+
*/
|
|
470
|
+
const fromLib = kind === "headless" && node.from
|
|
471
|
+
? { from: String(node.from), refName: tag }
|
|
472
|
+
: {};
|
|
439
473
|
const built = {
|
|
440
474
|
as,
|
|
441
475
|
...(name ? { name } : {}),
|
|
442
476
|
...(classes ? { at: index } : {}),
|
|
477
|
+
...fromLib,
|
|
443
478
|
...(node.text ? { text: node.text } : {}),
|
|
444
479
|
};
|
|
445
480
|
const inside = hasKids ? descend() : [];
|
package/dist/anatomy-read.js
CHANGED
|
@@ -265,6 +265,17 @@ sketch) {
|
|
|
265
265
|
continue;
|
|
266
266
|
}
|
|
267
267
|
const node_ = { as };
|
|
268
|
+
/**
|
|
269
|
+
* A PART THAT IS A LIBRARY ELEMENT CARRIES BOTH FACTS - see `AnatomyRead.from`.
|
|
270
|
+
* Inert for the renderer, which dispatches on `as`; read by the usage snippet,
|
|
271
|
+
* which has to emit `<Collapsible.Trigger>` and the import line rather than a
|
|
272
|
+
* `<div>` that looks right and does nothing.
|
|
273
|
+
*/
|
|
274
|
+
if (typeof node.from === "string" && node.from.trim()) {
|
|
275
|
+
node_.from = node.from.trim();
|
|
276
|
+
if (typeof node.refName === "string" && node.refName.trim())
|
|
277
|
+
node_.refName = node.refName.trim();
|
|
278
|
+
}
|
|
268
279
|
// The NAME is what carries the styles, so it is claimed even when nothing
|
|
269
280
|
// resolved: the tree triggers on presence, and structure is most of the
|
|
270
281
|
// value. A node with no name is pure structure and that is legitimate.
|
|
@@ -384,7 +395,34 @@ sketch) {
|
|
|
384
395
|
? (frontierClasses(rootObj, sketch).classes ??
|
|
385
396
|
"")
|
|
386
397
|
: "";
|
|
387
|
-
|
|
398
|
+
/**
|
|
399
|
+
* THE OVERRIDE TRAVELS AS DECLARATIONS, AND ONLY WHEN IT IS AN OVERRIDE.
|
|
400
|
+
*
|
|
401
|
+
* Two different things were sharing one field, and both were broken by it.
|
|
402
|
+
*
|
|
403
|
+
* When the root is a FRONTIER of theirs, the class string at the call site is what
|
|
404
|
+
* the PARENT paints on it - `MetricCard` returns `<Card className="flex-1
|
|
405
|
+
* dark:bg-darkgray-300">` - and this is the whole reason the field exists. It was
|
|
406
|
+
* sent as the class NAMES, so the platform pasted `dark:bg-darkgray-300` onto its own
|
|
407
|
+
* element and the browser resolved it against OUR Tailwind, which has no such colour:
|
|
408
|
+
* the override did nothing, on 5 of 5 of their components (dono, 03/08). Transcribed,
|
|
409
|
+
* it is a declaration that applies anywhere.
|
|
410
|
+
*
|
|
411
|
+
* When the root is one we DRAW - a headless primitive, a plain element - the classes
|
|
412
|
+
* at that index are the component's OWN look and are already in `base`. Sending them
|
|
413
|
+
* again put their raw `bg-white` on our element, in a layer that outranks the compiled
|
|
414
|
+
* recipe, and two of their cards previewed white in dark mode over a recipe that said
|
|
415
|
+
* otherwise. So a drawn root sends nothing here at all.
|
|
416
|
+
*/
|
|
417
|
+
const rootStyle = rootPainted
|
|
418
|
+
? transcribe(rootPainted.split(/\s+/).filter(Boolean), declared)
|
|
419
|
+
: null;
|
|
420
|
+
const paints = rootStyle && Object.keys(rootStyle.base).length > 0
|
|
421
|
+
? { style: rootStyle.base }
|
|
422
|
+
: {};
|
|
423
|
+
const paintsDark = rootStyle && Object.keys(rootStyle.dark).length > 0
|
|
424
|
+
? { dark: rootStyle.dark }
|
|
425
|
+
: {};
|
|
388
426
|
/**
|
|
389
427
|
* A LIBRARY ROOT WHOSE NAME IS NOT DOTTED.
|
|
390
428
|
*
|
|
@@ -416,7 +454,8 @@ sketch) {
|
|
|
416
454
|
const looksLikeTag = sent !== "" && /^[A-Z]/.test(sent);
|
|
417
455
|
const rootFrom = looksLikeTag ? (rootPackage ?? "") : sent;
|
|
418
456
|
if (rootFrom) {
|
|
419
|
-
|
|
457
|
+
// A root we draw carries no override: its classes are the component's own.
|
|
458
|
+
rootOut = { from: rootFrom };
|
|
420
459
|
}
|
|
421
460
|
else if (rootRaw && /^[A-Z]/.test(rootRaw)) {
|
|
422
461
|
if (!rootRaw.includes(".")) {
|
|
@@ -428,10 +467,10 @@ sketch) {
|
|
|
428
467
|
*/
|
|
429
468
|
const slug = resolve?.(rootRaw) ?? safePartName(rootRaw);
|
|
430
469
|
if (slug)
|
|
431
|
-
rootOut = { ref: slug, name: rootRaw, ...paints };
|
|
470
|
+
rootOut = { ref: slug, name: rootRaw, ...paints, ...paintsDark };
|
|
432
471
|
}
|
|
433
472
|
else if (rootPackage) {
|
|
434
|
-
rootOut = { from: rootPackage
|
|
473
|
+
rootOut = { from: rootPackage };
|
|
435
474
|
}
|
|
436
475
|
}
|
|
437
476
|
/**
|
package/dist/commands/import.js
CHANGED
|
@@ -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
|
-
|
|
1797
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
1867
|
-
|
|
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 } : {}),
|
|
@@ -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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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