synthesisui 0.16.119 → 0.16.121
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 +54 -15
- package/dist/anatomy-read.js +20 -0
- package/dist/commands/import.js +82 -30
- package/dist/doctor/data-contract.js +170 -0
- package/dist/doctor/sketch.js +81 -0
- package/dist/guide.js +191 -1
- package/dist/library-structure.js +23 -2
- package/package.json +1 -1
|
@@ -317,27 +317,30 @@ versionOf) {
|
|
|
317
317
|
* somebody pasting the snippet got a card that never opens (dono, 03/08).
|
|
318
318
|
*
|
|
319
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.
|
|
321
|
-
*
|
|
320
|
+
* the Canvas to draw, and an element the snippet emits.
|
|
321
|
+
*
|
|
322
|
+
* A MOTION WRAPPER IS THE SAME CASE, and it was flattened on the grounds that its
|
|
323
|
+
* element is "genuinely absent from the output". True of the Canvas, which draws no box
|
|
324
|
+
* for it; false of the code, where `AnimatePresence` is the difference between an
|
|
325
|
+
* `exit` animation that runs and one that cannot. Their panel carries
|
|
326
|
+
* `exit={{ height: 0 }}`, we copied it faithfully into the snippet, and with the
|
|
327
|
+
* wrapper flattened away it could never fire - the element unmounts before anything
|
|
328
|
+
* animates (dono, 04/08).
|
|
322
329
|
*/
|
|
323
|
-
if (kind === "headless"
|
|
330
|
+
if ((kind === "headless" || kind === "motion") &&
|
|
331
|
+
(rendersNothing(tag, classes) || (!classes && children.length > 0))) {
|
|
324
332
|
const held = children.flatMap((child) => build(child, depth + 1));
|
|
325
333
|
return [
|
|
326
334
|
{
|
|
327
335
|
as: held.length > 0 ? "stack" : "text",
|
|
328
336
|
from: String(node.from ?? tag),
|
|
329
337
|
refName: tag,
|
|
338
|
+
/** Its own index, so its guard and its props are still reachable - see `at`. */
|
|
339
|
+
at: index,
|
|
330
340
|
...(held.length > 0 ? { children: held } : {}),
|
|
331
341
|
},
|
|
332
342
|
];
|
|
333
343
|
}
|
|
334
|
-
if (kind === "motion" && rendersNothing(tag, classes)) {
|
|
335
|
-
return children.flatMap((child) => build(child, depth));
|
|
336
|
-
}
|
|
337
|
-
// `<AnimatePresence>` and friends: no element of their own.
|
|
338
|
-
if (kind === "motion" && !classes && children.length > 0) {
|
|
339
|
-
return children.flatMap((child) => build(child, depth));
|
|
340
|
-
}
|
|
341
344
|
const descend = () => children.flatMap((child) => build(child, depth + 1));
|
|
342
345
|
/**
|
|
343
346
|
* THEIR OWN COMPONENT, and a library that draws itself: a reference, and it
|
|
@@ -349,7 +352,20 @@ versionOf) {
|
|
|
349
352
|
{
|
|
350
353
|
as: "component",
|
|
351
354
|
ref: tag,
|
|
352
|
-
|
|
355
|
+
/**
|
|
356
|
+
* THE INDEX IS THE NODE'S IDENTITY ON THE SKETCH, not a pointer to its classes.
|
|
357
|
+
*
|
|
358
|
+
* Gated on `classes`, it meant a class-less node had no way back to what the
|
|
359
|
+
* sketch measured about it - and that is five facts, not one: the guard, the
|
|
360
|
+
* resting verdict, the animation props, the expression it writes and the props
|
|
361
|
+
* that make it inert. Their `Collapsible.Panel` carries no class and is guarded
|
|
362
|
+
* by `isOpen && !isLoading`, so the snippet showed an open panel as the resting
|
|
363
|
+
* state of a card that starts closed (dono, 04/08).
|
|
364
|
+
*
|
|
365
|
+
* Costs nothing: the two places that read `at` for styles take the classes at
|
|
366
|
+
* that index, and at a class-less index that is the empty string it already was.
|
|
367
|
+
*/
|
|
368
|
+
at: index,
|
|
353
369
|
...(children.length > 0 ? { children: descend() } : {}),
|
|
354
370
|
},
|
|
355
371
|
];
|
|
@@ -364,13 +380,30 @@ versionOf) {
|
|
|
364
380
|
{
|
|
365
381
|
as: "external",
|
|
366
382
|
from: String(node.from ?? tag),
|
|
367
|
-
|
|
383
|
+
at: index,
|
|
368
384
|
...(children.length > 0 ? { children: descend() } : {}),
|
|
369
385
|
},
|
|
370
386
|
];
|
|
371
387
|
}
|
|
372
388
|
if (kind === "icon") {
|
|
373
|
-
|
|
389
|
+
/**
|
|
390
|
+
* AN ICON IS THEIR ELEMENT WEARING OUR CLASS.
|
|
391
|
+
*
|
|
392
|
+
* `<ChevronUp className="size-5 …" />` - the glyph is the library's and every pixel
|
|
393
|
+
* of size and colour is in their class list, which is why it is a part of ours. The
|
|
394
|
+
* snippet emitted `<span className="ds-collapsible-card-chevron-up">…</span>`: the
|
|
395
|
+
* right box in the right place, drawing nothing, and nothing in his repo spelled
|
|
396
|
+
* that way (dono, 04/08). The name of the part is already their tag; the element
|
|
397
|
+
* should be too.
|
|
398
|
+
*/
|
|
399
|
+
return [
|
|
400
|
+
{
|
|
401
|
+
as: "icon",
|
|
402
|
+
name: nameOfTag(tag),
|
|
403
|
+
at: index,
|
|
404
|
+
...(node.from ? { from: String(node.from), refName: tag } : {}),
|
|
405
|
+
},
|
|
406
|
+
];
|
|
374
407
|
}
|
|
375
408
|
/**
|
|
376
409
|
* EVERYTHING ELSE IS DRAWN: plain HTML, a headless primitive, and a
|
|
@@ -467,13 +500,19 @@ versionOf) {
|
|
|
467
500
|
* The package AND their own spelling, because the snippet needs both - the import
|
|
468
501
|
* line comes from the first and the tag from the second.
|
|
469
502
|
*/
|
|
470
|
-
|
|
503
|
+
/**
|
|
504
|
+
* MOTION COUNTS THE SAME WAY. `AnimatePresence` carries no class and draws no pixel,
|
|
505
|
+
* and it is what lets the panel's `exit` animation run at all - without it the exit
|
|
506
|
+
* props are copied into the snippet and can never fire, because the element unmounts
|
|
507
|
+
* before anything animates. Headless-only dropped it (dono, 04/08).
|
|
508
|
+
*/
|
|
509
|
+
const fromLib = (kind === "headless" || kind === "motion") && node.from
|
|
471
510
|
? { from: String(node.from), refName: tag }
|
|
472
511
|
: {};
|
|
473
512
|
const built = {
|
|
474
513
|
as,
|
|
475
514
|
...(name ? { name } : {}),
|
|
476
|
-
|
|
515
|
+
at: index,
|
|
477
516
|
...fromLib,
|
|
478
517
|
...(node.text ? { text: node.text } : {}),
|
|
479
518
|
};
|
package/dist/anatomy-read.js
CHANGED
|
@@ -278,6 +278,16 @@ sketch) {
|
|
|
278
278
|
const atRest = node.atRest ?? measured?.atRest;
|
|
279
279
|
if (atRest === false)
|
|
280
280
|
node_.atRest = false;
|
|
281
|
+
/**
|
|
282
|
+
* WHAT THE NODE WRITES AND WHICH BEHAVIOUR PROPS IT CARRIES - the two facts that
|
|
283
|
+
* turn a styled shell into a component somebody can rebuild. Same lookup as the
|
|
284
|
+
* guard, one line up: measured on the sketch, dropped by every seam after it.
|
|
285
|
+
*/
|
|
286
|
+
if (measured?.contentFrom)
|
|
287
|
+
node_.contentFrom = measured.contentFrom;
|
|
288
|
+
if (measured?.behaviourProps &&
|
|
289
|
+
Object.keys(measured.behaviourProps).length > 0)
|
|
290
|
+
node_.behaviourProps = measured.behaviourProps;
|
|
281
291
|
}
|
|
282
292
|
else {
|
|
283
293
|
if (node.when)
|
|
@@ -325,6 +335,16 @@ sketch) {
|
|
|
325
335
|
node_.from = measured.from;
|
|
326
336
|
node_.refName = measured.tag;
|
|
327
337
|
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* THE ANIMATION PROPS COME OFF THE SKETCH WHATEVER ELSE IS KNOWN.
|
|
341
|
+
*
|
|
342
|
+
* They used to hang off the branch that had to GUESS the element from the sketch, so
|
|
343
|
+
* a node whose reading already named its package - which is every restored library
|
|
344
|
+
* wrapper - lost them.
|
|
345
|
+
*/
|
|
346
|
+
if (typeof node.at === "number" && !node_.motionProps) {
|
|
347
|
+
const measured = sketch?.[node.at];
|
|
328
348
|
if (measured?.motionProps)
|
|
329
349
|
node_.motionProps = measured.motionProps;
|
|
330
350
|
}
|
package/dist/commands/import.js
CHANGED
|
@@ -14,6 +14,7 @@ import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, }
|
|
|
14
14
|
import { countShape, describeCoverage, summarizeCoverage, } from "../doctor/coverage.js";
|
|
15
15
|
import { crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } from "../doctor/crosswalk.js";
|
|
16
16
|
import { keyframeOffsets, moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
|
|
17
|
+
import { dataContract } from "../doctor/data-contract.js";
|
|
17
18
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
18
19
|
import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
|
|
19
20
|
import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
|
|
@@ -24,7 +25,7 @@ import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "
|
|
|
24
25
|
import { sketchOf } from "../doctor/sketch.js";
|
|
25
26
|
import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
|
|
26
27
|
import { buildTable } from "../doctor/tokens.js";
|
|
27
|
-
import { readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
28
|
+
import { definitionSpan, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
28
29
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
29
30
|
import { frontierKind } from "../frontier-kind.js";
|
|
30
31
|
import { withLibraryStructure } from "../library-structure.js";
|
|
@@ -441,6 +442,7 @@ export async function takeCensus(root, opts) {
|
|
|
441
442
|
...et,
|
|
442
443
|
...(etag ? { rootTag: etag } : {}),
|
|
443
444
|
...rootPackage(etag, esketch),
|
|
445
|
+
...rootBehaviour(etag, esketch),
|
|
444
446
|
};
|
|
445
447
|
}
|
|
446
448
|
}
|
|
@@ -659,6 +661,7 @@ export async function takeCensus(root, opts) {
|
|
|
659
661
|
...(tree.length > 0 ? { tree } : {}),
|
|
660
662
|
...(tag ? { rootTag: tag } : {}),
|
|
661
663
|
...rootPackage(tag, sketch),
|
|
664
|
+
...rootBehaviour(tag, sketch),
|
|
662
665
|
...(placed.length > 0 ? { layers: placed } : {}),
|
|
663
666
|
...(Object.keys(islandStyle).length > 0 ? { islandStyle } : {}),
|
|
664
667
|
...(v.unslotted.length > 0
|
|
@@ -1206,35 +1209,63 @@ export async function takeCensus(root, opts) {
|
|
|
1206
1209
|
* as not exported. See `publicApi`.
|
|
1207
1210
|
*/
|
|
1208
1211
|
const api = await publicApi(root);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1212
|
+
for (const c of components) {
|
|
1213
|
+
const file = defined.find((d) => d.name === c.name)?.file;
|
|
1214
|
+
const source = file
|
|
1215
|
+
? sources.find((s) => s.file.endsWith(file))?.source
|
|
1216
|
+
: undefined;
|
|
1217
|
+
const required = source ? requiredProps(source, c.name) : [];
|
|
1218
|
+
/**
|
|
1219
|
+
* THE COMPONENT'S DATA CONTRACT - its props, its derivations, the copy an option
|
|
1220
|
+
* carries, and the type that names an axis's options.
|
|
1221
|
+
*
|
|
1222
|
+
* The owner asked whether the recipe was enough to rebuild the component in a new
|
|
1223
|
+
* project, and it was not: everything missing was LOGIC, and all of it sits in the
|
|
1224
|
+
* file already open here. See `dataContract`.
|
|
1225
|
+
*
|
|
1226
|
+
* Stamped whether or not the folder is a package: a `src/components` with no
|
|
1227
|
+
* manifest of its own has no specifier to offer and its components still have props.
|
|
1228
|
+
*/
|
|
1229
|
+
const span = source ? definitionSpan(source, c.name) : null;
|
|
1230
|
+
const contract = source
|
|
1231
|
+
? dataContract(source, span ? source.slice(span.from, span.to) : source, c.name, (c.declaredAxes ?? {}))
|
|
1232
|
+
: null;
|
|
1233
|
+
c.api = {
|
|
1234
|
+
/**
|
|
1235
|
+
* SILENCE WHEN THE ENTRY WAS NOT READ. An unreadable entry gives an empty export
|
|
1236
|
+
* set, and calling that "internal" would have marked every component internal at
|
|
1237
|
+
* once - see `PublicApi.read`.
|
|
1238
|
+
*/
|
|
1239
|
+
...(!api?.read
|
|
1240
|
+
? {}
|
|
1241
|
+
: api.exports.has(c.name)
|
|
1242
|
+
? { from: api.from }
|
|
1243
|
+
: { exported: false }),
|
|
1244
|
+
/**
|
|
1245
|
+
* REPO-RELATIVE, so the answer needs no second search. `defined[].file` is
|
|
1246
|
+
* relative to the scope; joined with the scope it is a path anybody can check
|
|
1247
|
+
* with one `stat` - which is how `component` knows they already have it without
|
|
1248
|
+
* walking a monorepo (their component sits six levels below `packages/`, and the
|
|
1249
|
+
* shallow walk that guards against shadowing never reached it).
|
|
1250
|
+
*/
|
|
1251
|
+
...(file ? { file: scopeLabel ? `${scopeLabel}/${file}` : file } : {}),
|
|
1252
|
+
...(required.length > 0 ? { required } : {}),
|
|
1253
|
+
...(contract && contract.props.length > 0
|
|
1254
|
+
? { props: contract.props }
|
|
1255
|
+
: {}),
|
|
1256
|
+
...(contract && Object.keys(contract.derived).length > 0
|
|
1257
|
+
? { derived: contract.derived }
|
|
1258
|
+
: {}),
|
|
1259
|
+
...(contract && Object.keys(contract.optionText).length > 0
|
|
1260
|
+
? { optionText: contract.optionText }
|
|
1261
|
+
: {}),
|
|
1262
|
+
...(contract && Object.keys(contract.axisType).length > 0
|
|
1263
|
+
? { axisType: contract.axisType }
|
|
1264
|
+
: {}),
|
|
1265
|
+
};
|
|
1266
|
+
// An api with nothing in it is not a fact - it is an empty object on 36 components.
|
|
1267
|
+
if (Object.keys(c.api).length === 0)
|
|
1268
|
+
c.api = undefined;
|
|
1238
1269
|
}
|
|
1239
1270
|
const brokenRefs = findBrokenRefs(sources, declaredNames);
|
|
1240
1271
|
const conventions = detectConventions(sources);
|
|
@@ -1878,6 +1909,27 @@ function rootPackage(tag, sketch) {
|
|
|
1878
1909
|
? { rootFrom: first.from }
|
|
1879
1910
|
: {};
|
|
1880
1911
|
}
|
|
1912
|
+
/**
|
|
1913
|
+
* THE PROPS ON THE ELEMENT THE COMPONENT RETURNS, when their value is a condition.
|
|
1914
|
+
*
|
|
1915
|
+
* 6 of their 27 conditional props sit on the root, and every one of the six is the prop
|
|
1916
|
+
* that makes the component controlled: `open={isOpen && !isLoading}` on their
|
|
1917
|
+
* `Collapsible.Root`, `checked={isChecked}` on a `Switch.Root`, `disabled` on a
|
|
1918
|
+
* `Field.Root`. Those are what somebody rebuilding it in another project has to wire, and
|
|
1919
|
+
* the root travelled with its tag, its package and its classes and none of its props
|
|
1920
|
+
* (dono, 04/08).
|
|
1921
|
+
*
|
|
1922
|
+
* Off the FIRST sketch node, which is the returned element - the same lookup `rootPackage`
|
|
1923
|
+
* makes one line up, guarded the same way.
|
|
1924
|
+
*/
|
|
1925
|
+
function rootBehaviour(tag, sketch) {
|
|
1926
|
+
const first = sketch[0];
|
|
1927
|
+
if (!tag || !first || first.tag !== tag)
|
|
1928
|
+
return {};
|
|
1929
|
+
return first.behaviourProps && Object.keys(first.behaviourProps).length > 0
|
|
1930
|
+
? { rootProps: first.behaviourProps }
|
|
1931
|
+
: {};
|
|
1932
|
+
}
|
|
1881
1933
|
async function resolveReadParts(census, root) {
|
|
1882
1934
|
/**
|
|
1883
1935
|
* A READING IS OPTIONAL HERE. THE SKETCH IS NOT.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE COMPONENT'S DATA CONTRACT - what it takes, what it derives, what it writes.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline reads appearance and structure well, and a recipe made of those two can be
|
|
5
|
+
* RECONSTRUCTED here and not REGENERATED somewhere else. The owner asked for the second
|
|
6
|
+
* thing (dono, 04/08): from the recipe, in a new project, Claude should write a component
|
|
7
|
+
* that behaves the same in that project's own vocabulary.
|
|
8
|
+
*
|
|
9
|
+
* What was missing is all logic, and all of it is in the file we already open:
|
|
10
|
+
*
|
|
11
|
+
* the props, with their types and which are required 29 of their 38 declare one
|
|
12
|
+
* the derivations `const isLoading = status === "loading"` 9
|
|
13
|
+
* the text a lookup table holds `statusConfig.waiting.text = "Waiting"` 6
|
|
14
|
+
* the exported type of an axis `type CollapsibleCardStatus = "waiting" | …` 3
|
|
15
|
+
*
|
|
16
|
+
* NONE OF IT IS INFERRED. A prop with no type says no type; a table with no text says no
|
|
17
|
+
* text. The one thing this refuses to do is invent a shape - a component whose props come
|
|
18
|
+
* from `React.ComponentProps<"div">` gets an empty list, because guessing which props
|
|
19
|
+
* somebody must pass is worse than saying nothing.
|
|
20
|
+
*/
|
|
21
|
+
/** `export type CollapsibleCardStatus = "waiting" | "fail" | …` */
|
|
22
|
+
const UNION = /export\s+type\s+([A-Za-z_$][\w$]*)\s*=\s*((?:"[^"]*"\s*\|\s*)+"[^"]*")\s*;/g;
|
|
23
|
+
/** The first `{ … }` of a `({ … })` parameter list, balanced. */
|
|
24
|
+
function paramBlock(body) {
|
|
25
|
+
const open = body.indexOf("({");
|
|
26
|
+
if (open === -1)
|
|
27
|
+
return null;
|
|
28
|
+
let depth = 0;
|
|
29
|
+
for (let i = open + 1; i < body.length; i++) {
|
|
30
|
+
if (body[i] === "{")
|
|
31
|
+
depth++;
|
|
32
|
+
else if (body[i] === "}") {
|
|
33
|
+
depth--;
|
|
34
|
+
if (depth === 0)
|
|
35
|
+
return body.slice(open + 2, i);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Split on top-level commas, so `{ a = { b: 1 }, c }` reads as two entries. */
|
|
41
|
+
function topLevel(text) {
|
|
42
|
+
const out = [];
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let part = "";
|
|
45
|
+
for (const c of text) {
|
|
46
|
+
if ("([{".includes(c))
|
|
47
|
+
depth++;
|
|
48
|
+
else if (")]}".includes(c))
|
|
49
|
+
depth--;
|
|
50
|
+
if (c === "," && depth === 0) {
|
|
51
|
+
out.push(part);
|
|
52
|
+
part = "";
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
part += c;
|
|
56
|
+
}
|
|
57
|
+
out.push(part);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Their props interface, field by field - name, type, and whether it is required.
|
|
62
|
+
*
|
|
63
|
+
* Named after the component when they name it that way (29 of their 38 do), and the first
|
|
64
|
+
* `*Props` in the file otherwise. Nothing at all when they extend a library's type: the
|
|
65
|
+
* fields are not there to read and a list of ours would be an invention.
|
|
66
|
+
*/
|
|
67
|
+
function propsFromType(source, name) {
|
|
68
|
+
const scoped = new RegExp(`(?:export\\s+)?(?:interface|type)\\s+${name}Props\\s*(?:extends [^{]+)?(?:=\\s*)?\\{([\\s\\S]*?)\\n\\}`).exec(source);
|
|
69
|
+
const generic = /(?:export\s+)?(?:interface|type)\s+\w*Props\s*(?:extends [^{]+)?(?:=\s*)?\{([\s\S]*?)\n\}/.exec(source);
|
|
70
|
+
const block = scoped?.[1] ?? generic?.[1];
|
|
71
|
+
if (!block)
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const line of block.split("\n")) {
|
|
75
|
+
const m = /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)(\?)?\s*:\s*(.+?);?\s*$/.exec(line);
|
|
76
|
+
if (!m)
|
|
77
|
+
continue;
|
|
78
|
+
const type = m[3].replace(/;$/, "").trim();
|
|
79
|
+
out.push({
|
|
80
|
+
name: m[1],
|
|
81
|
+
...(type ? { type: type.slice(0, 80) } : {}),
|
|
82
|
+
...(m[2] ? {} : { required: true }),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* THE WHOLE CONTRACT, from the file and the component's own body.
|
|
89
|
+
*
|
|
90
|
+
* `body` is the definition span - the component's own function - so a file that declares
|
|
91
|
+
* two components does not lend one of them the other's defaults. `source` is the whole
|
|
92
|
+
* file, because a type and a lookup table live outside the function.
|
|
93
|
+
*/
|
|
94
|
+
export function dataContract(source, body, name,
|
|
95
|
+
/** Axis → options, from the census - so a table is only read against a real axis. */
|
|
96
|
+
axes) {
|
|
97
|
+
const props = propsFromType(source, name);
|
|
98
|
+
const byName = new Map(props.map((p) => [p.name, p]));
|
|
99
|
+
/** The destructuring says which props have a default, and names the ones with no type. */
|
|
100
|
+
const block = paramBlock(body);
|
|
101
|
+
if (block) {
|
|
102
|
+
for (const entry of topLevel(block)) {
|
|
103
|
+
const eq = entry.indexOf("=");
|
|
104
|
+
const key = (eq === -1 ? entry : entry.slice(0, eq)).split(":")[0].trim();
|
|
105
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key))
|
|
106
|
+
continue;
|
|
107
|
+
const found = byName.get(key) ?? { name: key };
|
|
108
|
+
if (!byName.has(key)) {
|
|
109
|
+
props.push(found);
|
|
110
|
+
byName.set(key, found);
|
|
111
|
+
}
|
|
112
|
+
if (eq !== -1) {
|
|
113
|
+
const value = entry.slice(eq + 1).trim();
|
|
114
|
+
if (value)
|
|
115
|
+
found.default = value.slice(0, 60);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* THE DERIVATIONS. Only the ones whose expression is a CONDITION - a comparison or a
|
|
121
|
+
* boolean chain - because those are what the conditional look gates on. A `const config
|
|
122
|
+
* = statusConfig[status]` is a lookup, and the table it reads is captured below instead.
|
|
123
|
+
*/
|
|
124
|
+
const derived = {};
|
|
125
|
+
for (const m of body.matchAll(/const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*([^;\n]*(?:===|!==|&&|\|\|)[^;\n]*);/g)) {
|
|
126
|
+
const expression = m[2].trim();
|
|
127
|
+
if (!derived[m[1]] && expression.length <= 120)
|
|
128
|
+
derived[m[1]] = expression;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* THE COPY A LOOKUP TABLE HOLDS, per axis option. Read only against an axis the census
|
|
132
|
+
* declared, and only for a field whose value is prose rather than a class list - which is
|
|
133
|
+
* what separates `text: "Waiting"` from `dotClass: "bg-lightgray-500"`.
|
|
134
|
+
*/
|
|
135
|
+
const optionText = {};
|
|
136
|
+
for (const table of source.matchAll(/const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\{([\s\S]*?)\n\}\s*;/g)) {
|
|
137
|
+
const axis = Object.keys(axes).find((a) => new RegExp(`\\b${table[1]}\\s*\\[\\s*${a}\\s*\\]`).test(source));
|
|
138
|
+
if (!axis)
|
|
139
|
+
continue;
|
|
140
|
+
for (const entry of topLevel(table[2])) {
|
|
141
|
+
const option = /^\s*"?([\w-]+)"?\s*:\s*\{([\s\S]*)\}\s*$/.exec(entry);
|
|
142
|
+
if (!option || !axes[axis].includes(option[1]))
|
|
143
|
+
continue;
|
|
144
|
+
for (const field of topLevel(option[2])) {
|
|
145
|
+
const kv = /^\s*(\w+)\s*:\s*"([^"]+)"\s*$/.exec(field);
|
|
146
|
+
if (!kv)
|
|
147
|
+
continue;
|
|
148
|
+
// A class list is a look and travels as one; prose is copy.
|
|
149
|
+
if (/^(?:[a-z-]+:)?[a-z][\w-]*(?:\s+(?:[a-z-]+:)?[a-z][\w-]*)*$/.test(kv[2]) &&
|
|
150
|
+
/class/i.test(kv[1]))
|
|
151
|
+
continue;
|
|
152
|
+
if (/class/i.test(kv[1]))
|
|
153
|
+
continue;
|
|
154
|
+
(optionText[axis] ??= {})[option[1]] = kv[2].slice(0, 80);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** The exported union that lists an axis's options, when they declared one. */
|
|
159
|
+
const axisType = {};
|
|
160
|
+
UNION.lastIndex = 0;
|
|
161
|
+
for (const m of source.matchAll(UNION)) {
|
|
162
|
+
const options = [...m[2].matchAll(/"([^"]*)"/g)].map((o) => o[1]);
|
|
163
|
+
for (const [axis, declared] of Object.entries(axes)) {
|
|
164
|
+
if (declared.length === options.length &&
|
|
165
|
+
declared.every((o) => options.includes(o)))
|
|
166
|
+
axisType[axis] = m[1];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { props, derived, optionText, axisType };
|
|
170
|
+
}
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -280,6 +280,77 @@ export function motionPropsOf(body) {
|
|
|
280
280
|
const out = parts.join(" ");
|
|
281
281
|
return out.length > 400 ? null : out;
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* The expression an element writes as its own content - `{title}`, `{a || b.c}`.
|
|
285
|
+
*
|
|
286
|
+
* Only what sits DIRECTLY between this tag and the next event, so a parent does not claim
|
|
287
|
+
* its child's content. `{children}` is the caller's and already travels as a slot.
|
|
288
|
+
*/
|
|
289
|
+
export function contentExpressionOf(source, open) {
|
|
290
|
+
// A tag that closes itself has no content, and what follows it is its PARENT's.
|
|
291
|
+
if (open.body.trimEnd().endsWith("/"))
|
|
292
|
+
return null;
|
|
293
|
+
/** `<` + name + body + `>` - the scan reports `body` without the name or the brackets. */
|
|
294
|
+
const from = open.at + 1 + open.tag.length + open.body.length + 1;
|
|
295
|
+
/**
|
|
296
|
+
* UP TO THIS ELEMENT'S OWN CLOSING TAG, and no further. Bounding by the next OPENING tag
|
|
297
|
+
* instead swept `</h3>` into the slice, and every one of their 22 read as nothing.
|
|
298
|
+
*
|
|
299
|
+
* A slice that does not close - `{isLoading && (` before a nested element - fails the
|
|
300
|
+
* balanced test below and says nothing, which is right: that is a guard, and a guard
|
|
301
|
+
* already travels as `when`.
|
|
302
|
+
*/
|
|
303
|
+
const close = source.indexOf("<", from);
|
|
304
|
+
const to = Math.min(close === -1 ? source.length : close, from + 200);
|
|
305
|
+
if (to <= from)
|
|
306
|
+
return null;
|
|
307
|
+
const between = source.slice(from, to);
|
|
308
|
+
const m = /^\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\s*$/.exec(between);
|
|
309
|
+
if (!m)
|
|
310
|
+
return null;
|
|
311
|
+
const expression = m[1].replace(/\s+/g, " ").trim();
|
|
312
|
+
if (!expression || expression === "children")
|
|
313
|
+
return null;
|
|
314
|
+
/**
|
|
315
|
+
* `{/* Header Section *``/}` IS A COMMENT, not content. Their WidgetCard opens with one
|
|
316
|
+
* and it arrived as the expression its root writes - a note to a human, presented as the
|
|
317
|
+
* prop that feeds an element.
|
|
318
|
+
*/
|
|
319
|
+
if (expression.startsWith("/*") || expression.startsWith("//"))
|
|
320
|
+
return null;
|
|
321
|
+
// A whole JSX subtree is not content; a call is not a name we can attribute.
|
|
322
|
+
if (/[<>]/.test(expression) || expression.length > 90)
|
|
323
|
+
return null;
|
|
324
|
+
return expression;
|
|
325
|
+
}
|
|
326
|
+
/** The state props: `disabled={isLoading}`, `open={isOpen && !isLoading}`. */
|
|
327
|
+
const STATE_PROP = /\b(disabled|readOnly|checked|open|selected|required|expanded|active|loading|indeterminate)=\{([^}]{1,60})\}/g;
|
|
328
|
+
/**
|
|
329
|
+
* The handler that closes the loop on one of them: `onOpenChange={setIsOpen}`.
|
|
330
|
+
*
|
|
331
|
+
* A controlled `open` with no `onOpenChange` is a disclosure that opens once and never
|
|
332
|
+
* closes - which is what somebody pasting the snippet would have got. Only a NAME as the
|
|
333
|
+
* value: an inline arrow is a function body, and copying one means copying whatever it
|
|
334
|
+
* calls, which is not in the recipe.
|
|
335
|
+
*/
|
|
336
|
+
const HANDLER_PROP = /\b(on[A-Z]\w*)=\{([A-Za-z_$][\w$.]{0,40})\}/g;
|
|
337
|
+
export function conditionalPropsOf(body) {
|
|
338
|
+
const out = {};
|
|
339
|
+
STATE_PROP.lastIndex = 0;
|
|
340
|
+
for (const m of body.matchAll(STATE_PROP)) {
|
|
341
|
+
const value = m[2].replace(/\s+/g, " ").trim();
|
|
342
|
+
// A literal is not behaviour, and an inline function is a body we cannot copy.
|
|
343
|
+
if (/^(true|false)$/.test(value) || /=>/.test(value))
|
|
344
|
+
continue;
|
|
345
|
+
if (!out[m[1]])
|
|
346
|
+
out[m[1]] = value;
|
|
347
|
+
}
|
|
348
|
+
HANDLER_PROP.lastIndex = 0;
|
|
349
|
+
for (const m of body.matchAll(HANDLER_PROP))
|
|
350
|
+
if (!out[m[1]])
|
|
351
|
+
out[m[1]] = m[2].trim();
|
|
352
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
353
|
+
}
|
|
283
354
|
export function sketchOf(source, name) {
|
|
284
355
|
/**
|
|
285
356
|
* READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
|
|
@@ -352,6 +423,16 @@ export function sketchOf(source, name) {
|
|
|
352
423
|
const motion = motionPropsOf(event.body);
|
|
353
424
|
if (motion)
|
|
354
425
|
node.motionProps = motion;
|
|
426
|
+
/**
|
|
427
|
+
* WHICH PROP FEEDS THIS ELEMENT, and which behaviour props it carries - the two facts
|
|
428
|
+
* a class list cannot hold. See `contentFrom` and `behaviourProps`.
|
|
429
|
+
*/
|
|
430
|
+
const feeds = contentExpressionOf(source, event);
|
|
431
|
+
if (feeds)
|
|
432
|
+
node.contentFrom = feeds;
|
|
433
|
+
const conditional = conditionalPropsOf(event.body);
|
|
434
|
+
if (conditional)
|
|
435
|
+
node.behaviourProps = conditional;
|
|
355
436
|
// What has to be true for this element to exist at all - see `guardAt`.
|
|
356
437
|
const guard = guardAt(source, event.at);
|
|
357
438
|
if (guard) {
|
package/dist/guide.js
CHANGED
|
@@ -33,6 +33,102 @@ function customFontFamilies(families) {
|
|
|
33
33
|
}
|
|
34
34
|
return out;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* THE STRUCTURE, WITH WHAT EACH PART IS FOR AND WHEN IT EXISTS.
|
|
38
|
+
*
|
|
39
|
+
* A flat list of class names is enough to STYLE and not enough to BUILD: an agent in
|
|
40
|
+
* another project reading fifteen of them cannot tell which is the thing you press, which
|
|
41
|
+
* one holds the revealed content, or which two are not always there. The recipe carries
|
|
42
|
+
* both facts per node and the guide was printing neither (dono, 04/08).
|
|
43
|
+
*/
|
|
44
|
+
function anatomyLines(cname, nodes, depth = 1) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const node of nodes ?? []) {
|
|
47
|
+
if (depth > 6)
|
|
48
|
+
break;
|
|
49
|
+
const pad = " ".repeat(depth);
|
|
50
|
+
const name = node.part
|
|
51
|
+
? `\`.ds-${kebab(cname)}-${kebab(node.part)}\``
|
|
52
|
+
: node.refName
|
|
53
|
+
? `\`<${node.refName}>\``
|
|
54
|
+
: node.ref
|
|
55
|
+
? `your \`${node.refName ?? node.ref}\``
|
|
56
|
+
: node.as;
|
|
57
|
+
const facts = [node.as];
|
|
58
|
+
if (node.role && node.role !== node.as)
|
|
59
|
+
facts.push(node.role);
|
|
60
|
+
if (node.refName && node.from)
|
|
61
|
+
facts.push(`from \`${node.from}\``);
|
|
62
|
+
if (node.expects)
|
|
63
|
+
facts.push(`holds ${node.expects}`);
|
|
64
|
+
/**
|
|
65
|
+
* WHEN IT EXISTS, in the recipe's own words rather than in theirs - see
|
|
66
|
+
* `PreviewNode.only`. Their `isLoading` means nothing in another project; "only in a
|
|
67
|
+
* state this component has" is actionable anywhere.
|
|
68
|
+
*/
|
|
69
|
+
const only = node.only;
|
|
70
|
+
if (only && "axis" in only)
|
|
71
|
+
facts.push(`only when \`data-${kebab(only.axis)}="${only.option}"\``);
|
|
72
|
+
else if (only && "state" in only)
|
|
73
|
+
facts.push("only in that state");
|
|
74
|
+
else if (only && "caller" in only)
|
|
75
|
+
facts.push("only when you pass it");
|
|
76
|
+
/**
|
|
77
|
+
* WHICH PROP FEEDS IT, AND WHAT MAKES IT INERT - the link between a list of parts and
|
|
78
|
+
* a list of props, which nothing carried.
|
|
79
|
+
*
|
|
80
|
+
* `{title}` is their name, said as `writes` rather than as an instruction: an agent
|
|
81
|
+
* reading fifteen parts needs to know THIS is where the heading text goes, and the
|
|
82
|
+
* prop that carries it in the new project is that project's to name.
|
|
83
|
+
*/
|
|
84
|
+
if (node.contentFrom)
|
|
85
|
+
facts.push(`writes \`${node.contentFrom}\``);
|
|
86
|
+
for (const [prop, value] of Object.entries(node.behaviourProps ?? {}))
|
|
87
|
+
facts.push(`\`${prop}\` while \`${value}\``);
|
|
88
|
+
out.push(`${pad}- ${name} - ${facts.join(" · ")}`);
|
|
89
|
+
out.push(...anatomyLines(cname, node.children, depth + 1));
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* WHAT THE COMPONENT DOES, said without naming a package.
|
|
95
|
+
*
|
|
96
|
+
* The recipe says `@base-ui/react/collapsible`. In a project that uses Radix - or nothing -
|
|
97
|
+
* that is evidence and not an instruction. What travels is the SHAPE of the interaction,
|
|
98
|
+
* and it is derivable from two facts the recipe already carries: the behaviour belongs to
|
|
99
|
+
* a headless library, and one part has the `action` role while another holds the content.
|
|
100
|
+
*
|
|
101
|
+
* Derived, never invented: with no `action` part and no headless library there is nothing
|
|
102
|
+
* to say, and this says nothing.
|
|
103
|
+
*/
|
|
104
|
+
function behaviourLine(cname, recipe) {
|
|
105
|
+
const headless = (recipe.libraries ?? []).some((l) => l.kind === "headless");
|
|
106
|
+
if (!headless)
|
|
107
|
+
return null;
|
|
108
|
+
const flat = [];
|
|
109
|
+
const walk = (nodes) => {
|
|
110
|
+
for (const node of nodes ?? []) {
|
|
111
|
+
flat.push(node);
|
|
112
|
+
walk(node.children);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
walk(recipe.preview?.parts);
|
|
116
|
+
const action = flat.find((n) => n.role === "action");
|
|
117
|
+
if (!action)
|
|
118
|
+
return null;
|
|
119
|
+
/**
|
|
120
|
+
* A part that exists only in a STATE, or a slot, is the region an action reveals - which
|
|
121
|
+
* is what separates a disclosure from a button that merely has a name.
|
|
122
|
+
*/
|
|
123
|
+
const revealed = flat.find((n) => n.as === "slot" || (n.only && "state" in n.only));
|
|
124
|
+
const press = action.part
|
|
125
|
+
? `\`.ds-${kebab(cname)}-${kebab(action.part)}\``
|
|
126
|
+
: "the action";
|
|
127
|
+
if (!revealed) {
|
|
128
|
+
return `the interaction: ${press} is the pressable one - wire it yourself or with any headless primitive, and give it the ARIA the pattern calls for.`;
|
|
129
|
+
}
|
|
130
|
+
return `the interaction: ${press} is pressed and reveals the region below it. Whatever drives that - a headless library or your own state - the pressable element announces \`aria-expanded\` and points at the region with \`aria-controls\`; the region carries the matching \`id\`. This recipe styles both and drives neither.`;
|
|
131
|
+
}
|
|
36
132
|
/** One entry per component: class, variant data-*, states, and multi-part anatomy. */
|
|
37
133
|
function componentEntry(cname, recipe) {
|
|
38
134
|
const cls = `.ds-${kebab(cname)}`;
|
|
@@ -50,7 +146,101 @@ function componentEntry(cname, recipe) {
|
|
|
50
146
|
});
|
|
51
147
|
partsText = `\n parts: ${items.join(", ")}`;
|
|
52
148
|
}
|
|
53
|
-
|
|
149
|
+
/**
|
|
150
|
+
* EVERYTHING ELSE THE RECIPE ALREADY KNOWS, and this entry was throwing away.
|
|
151
|
+
*
|
|
152
|
+
* Measured on a real system: the recipe holds 15 fields and the guide read 4 of them, so
|
|
153
|
+
* an agent in another project got a list of class names and had to invent the rest -
|
|
154
|
+
* which is the difference between styling this component and building one that behaves
|
|
155
|
+
* like it (dono, 04/08).
|
|
156
|
+
*/
|
|
157
|
+
const facts = [];
|
|
158
|
+
if (recipe.tier)
|
|
159
|
+
facts.push(`a ${recipe.tier}`);
|
|
160
|
+
if (recipe.api?.name) {
|
|
161
|
+
facts.push(recipe.api.from
|
|
162
|
+
? `their own \`${recipe.api.name}\`, imported from \`${recipe.api.from}\``
|
|
163
|
+
: `their own \`${recipe.api.name}\``);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* WHO OWNS THE STATE, which is the first question anybody rebuilding it asks. A recipe
|
|
167
|
+
* that manages itself needs no props to work; one the caller drives needs the pair.
|
|
168
|
+
*/
|
|
169
|
+
const runtime = recipe.runtime;
|
|
170
|
+
if (runtime?.stateOwner) {
|
|
171
|
+
const owner = runtime.stateOwner === "self"
|
|
172
|
+
? "manages its own state"
|
|
173
|
+
: runtime.stateOwner === "caller"
|
|
174
|
+
? "the caller owns its state"
|
|
175
|
+
: "works controlled or uncontrolled";
|
|
176
|
+
const pairs = (runtime.controlled ?? [])
|
|
177
|
+
.map((c) => `\`${c.prop}\` + \`${c.event}\``)
|
|
178
|
+
.join(", ");
|
|
179
|
+
facts.push(pairs ? `${owner} (${pairs})` : owner);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* WHOSE BEHAVIOUR IT IS, and what that means for somebody rebuilding it. `headless` is
|
|
183
|
+
* the one that changes the answer: the pixels are in this recipe and the interaction is
|
|
184
|
+
* a library's, so ANY headless primitive of that shape will do - it does not have to be
|
|
185
|
+
* the one they used.
|
|
186
|
+
*/
|
|
187
|
+
for (const lib of recipe.libraries ?? []) {
|
|
188
|
+
if (lib.kind === "headless")
|
|
189
|
+
facts.push(`behaviour from \`${lib.from}\` - a headless library: every pixel is in this recipe, the interaction is not. Any equivalent primitive works`);
|
|
190
|
+
else if (lib.kind === "opaque")
|
|
191
|
+
facts.push(`\`${lib.from}\` draws part of it, and that part is not in this recipe`);
|
|
192
|
+
}
|
|
193
|
+
const defaults = Object.entries(recipe.defaults ?? {})
|
|
194
|
+
.map(([axis, option]) => `\`data-${kebab(axis)}="${option}"\``)
|
|
195
|
+
.join(", ");
|
|
196
|
+
const defaultsText = defaults ? `\n at rest: ${defaults}` : "";
|
|
197
|
+
const factsText = facts.length ? `\n ${facts.join("\n ")}` : "";
|
|
198
|
+
/** Its own laws, computed from its own usage - and nothing read them (dono, 04/08). */
|
|
199
|
+
const laws = (recipe.usage ?? []).map((law) => `\n - ${law}`).join("");
|
|
200
|
+
const lawsText = laws ? `\n it obeys:${laws}` : "";
|
|
201
|
+
const behaviour = behaviourLine(cname, recipe);
|
|
202
|
+
const behaviourText = behaviour ? `\n ${behaviour}` : "";
|
|
203
|
+
/**
|
|
204
|
+
* THE DATA CONTRACT, in prose an agent can act on.
|
|
205
|
+
*
|
|
206
|
+
* The owner's question was whether this recipe is enough to rebuild the component in a
|
|
207
|
+
* NEW project, and the answer was no: everything missing was logic. The classes and the
|
|
208
|
+
* nesting say what it looks like; these four lines say what it takes, what it computes,
|
|
209
|
+
* and what its options actually SAY (dono, 04/08).
|
|
210
|
+
*
|
|
211
|
+
* Their names, verbatim, presented as evidence: in the new project the props are named
|
|
212
|
+
* for that project's domain, and what transfers is the shape - which props are required,
|
|
213
|
+
* which condition gates which element, and what copy each option carries.
|
|
214
|
+
*/
|
|
215
|
+
const contract = [];
|
|
216
|
+
const props = recipe.api?.props ?? [];
|
|
217
|
+
if (props.length > 0) {
|
|
218
|
+
contract.push(`props, as their own type declares them: ${props
|
|
219
|
+
.map((prop) => `\`${prop.name}${prop.required ? "" : "?"}${prop.type ? `: ${prop.type}` : ""}\`${prop.default ? ` (default \`${prop.default}\`)` : ""}`)
|
|
220
|
+
.join(", ")}`);
|
|
221
|
+
}
|
|
222
|
+
const derived = Object.entries(recipe.api?.derived ?? {});
|
|
223
|
+
if (derived.length > 0) {
|
|
224
|
+
contract.push(`it computes ${derived
|
|
225
|
+
.map(([name_, expression]) => `\`${name_}\` = \`${expression}\``)
|
|
226
|
+
.join(", ")} - the conditions below are written in those names`);
|
|
227
|
+
}
|
|
228
|
+
for (const [axis, byOption] of Object.entries(recipe.api?.optionText ?? {})) {
|
|
229
|
+
const said = Object.entries(byOption)
|
|
230
|
+
.map(([option, text]) => `${option} → "${text}"`)
|
|
231
|
+
.join(", ");
|
|
232
|
+
if (said)
|
|
233
|
+
contract.push(`each \`${axis}\` carries its own copy: ${said} - reword these for the new domain, keep one per option`);
|
|
234
|
+
}
|
|
235
|
+
for (const [axis, type] of Object.entries(recipe.api?.axisType ?? {})) {
|
|
236
|
+
contract.push(`\`${axis}\` is a named union in their code (\`${type}\`) - export the same kind of type rather than inlining the strings`);
|
|
237
|
+
}
|
|
238
|
+
const contractText = contract.length ? `\n ${contract.join("\n ")}` : "";
|
|
239
|
+
const anatomy = anatomyLines(cname, recipe.preview?.parts);
|
|
240
|
+
const anatomyText = anatomy.length
|
|
241
|
+
? `\n anatomy, in this nesting:\n${anatomy.join("\n")}`
|
|
242
|
+
: "";
|
|
243
|
+
return `- **${cname}** (\`${cls}\`)${variantsText}\n ${recipe.description}${factsText}${defaultsText}${partsText}${statesText}${behaviourText}${contractText}${anatomyText}${lawsText}`;
|
|
54
244
|
}
|
|
55
245
|
/**
|
|
56
246
|
* Builds GUIDE.md - instructions *for the agent* on how to build components
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* it, and re-inserting it would be a second opinion about a styled element.
|
|
28
28
|
*/
|
|
29
29
|
import { frontierKind } from "./frontier-kind.js";
|
|
30
|
+
/** A restored wrapper points at its own sketch node - see the `at` stamped in `flush`. */
|
|
30
31
|
/** Index → the index of its parent, or -1. Read off `depth`, which is the nesting. */
|
|
31
32
|
function parents(sketch) {
|
|
32
33
|
const out = [];
|
|
@@ -49,13 +50,21 @@ function claimed(read, into = new Set()) {
|
|
|
49
50
|
}
|
|
50
51
|
return into;
|
|
51
52
|
}
|
|
52
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Behaviour with no appearance: their element, no class list, nobody's part.
|
|
55
|
+
*
|
|
56
|
+
* MOTION COUNTS AS BEHAVIOUR. `AnimatePresence` carries no class and draws no pixel, and
|
|
57
|
+
* it is the element that lets an `exit` animation run at all - without it the panel's
|
|
58
|
+
* `exit={{height:0}}` is copied into the snippet and can never fire, because the element
|
|
59
|
+
* unmounts before anything animates (dono, 04/08). Headless-only left it out.
|
|
60
|
+
*/
|
|
53
61
|
function isBehaviour(node) {
|
|
54
62
|
if (!node)
|
|
55
63
|
return false;
|
|
56
64
|
if ((node.classes ?? "").trim())
|
|
57
65
|
return false;
|
|
58
|
-
|
|
66
|
+
const kind = frontierKind(node.from);
|
|
67
|
+
return kind === "headless" || kind === "motion";
|
|
59
68
|
}
|
|
60
69
|
/**
|
|
61
70
|
* Put the missing library frame back around the nodes it wraps.
|
|
@@ -110,6 +119,18 @@ wrapped) {
|
|
|
110
119
|
as: "stack",
|
|
111
120
|
from: String(node.from ?? node.tag),
|
|
112
121
|
refName: node.tag,
|
|
122
|
+
/**
|
|
123
|
+
* THE INDEX TRAVELS WITH THE RESTORED ELEMENT, so everything the sketch measured
|
|
124
|
+
* about it reaches the recipe by the same lookup every named node uses: the
|
|
125
|
+
* guard, the resting fact, the animation props.
|
|
126
|
+
*
|
|
127
|
+
* Their panel is guarded - `{isOpen && !isLoading && (<Collapsible.Panel>` - and
|
|
128
|
+
* a restored wrapper with no index arrived unconditional, so the snippet showed
|
|
129
|
+
* an open panel as the resting state of a card that starts closed (dono, 04/08).
|
|
130
|
+
* It also makes this idempotent for free: a second pass finds the index already
|
|
131
|
+
* claimed.
|
|
132
|
+
*/
|
|
133
|
+
at,
|
|
113
134
|
children: built,
|
|
114
135
|
},
|
|
115
136
|
];
|
package/package.json
CHANGED