synthesisui 0.16.106 → 0.16.107

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.
@@ -468,10 +468,20 @@ versionOf) {
468
468
  * first one's classes became `base`, so the rest are emitted as siblings
469
469
  * instead of being dropped with the fragment.
470
470
  */
471
- const top = roots.length > 0 ? roots[0] : 0;
471
+ /**
472
+ * THE COMPONENT'S OWN ROOT, which is not always the first node.
473
+ *
474
+ * A file that declares a closure before the render it uses puts that closure's
475
+ * markup first: three of a real library's charts open with a `CustomTooltip`, so
476
+ * starting at node 0 made the component's anatomy the TOOLTIP's, with the real
477
+ * root arriving as a sibling (dono, 03/08). The reader stamps which node the
478
+ * component returns - see `SketchNode.root`.
479
+ */
480
+ const stamped = sketch.findIndex((node) => node?.root);
481
+ const top = stamped >= 0 ? stamped : roots.length > 0 ? roots[0] : 0;
472
482
  const read = [
473
483
  ...(kids[top] ?? []).flatMap((child) => build(child, 0)),
474
- ...roots.slice(1).flatMap((sibling) => build(sibling, 0)),
484
+ ...roots.filter((at) => at !== top).flatMap((sibling) => build(sibling, 0)),
475
485
  ];
476
486
  /**
477
487
  * THE ROOT'S OWN `{children}` - the component whose entire content is the
@@ -44,8 +44,18 @@ const FORMS = new Set([
44
44
  "external",
45
45
  "slot",
46
46
  ]);
47
- /** The two that arrange, and the only two that take children. */
47
+ /** The two that arrange, and the only two that take children of any kind. */
48
48
  const CONTAINERS = new Set(["row", "stack"]);
49
+ /**
50
+ * PHRASING CONTENT - what HTML lets an inline host hold.
51
+ *
52
+ * A glyph and a run of words. This is the list the containment law is actually
53
+ * about: a button may hold these and may not hold a heading, a panel or a second
54
+ * interactive node.
55
+ */
56
+ const PHRASING = new Set(["icon", "text", "image"]);
57
+ /** Forms that are inline hosts: they hold phrasing content and nothing else. */
58
+ const PHRASING_HOST = new Set(["button", "text", "heading"]);
49
59
  /** The two frontiers: no name, no styles - what is there is not ours to hold. */
50
60
  const FRONTIERS = new Set(["component", "external"]);
51
61
  /**
@@ -150,7 +160,9 @@ sketch) {
150
160
  * Node 0 of the sketch IS what the component returns, and the reader stamped its
151
161
  * import specifier there - see `SketchNode.from`.
152
162
  */
153
- const rootPackage = sketch?.[0]?.from ?? null;
163
+ const rootPackage = (sketch ?? []).find((node) => node?.root)?.from ??
164
+ sketch?.[0]?.from ??
165
+ null;
154
166
  const parts = {};
155
167
  const composes = [];
156
168
  const external = [];
@@ -300,9 +312,25 @@ sketch) {
300
312
  if (expects)
301
313
  node_.expects = expects.slice(0, 80);
302
314
  }
303
- // Only the two arrangers descend. A leaf with children is a leaf that was
304
- // labelled wrongly, and its children would render nowhere.
305
- if (CONTAINERS.has(as) && Array.isArray(node.children)) {
315
+ /**
316
+ * AN ACTION HOLDS ITS GLYPH, because that is valid HTML.
317
+ *
318
+ * `<button><Upload/>Upload</button>` is legal: a button's content model is
319
+ * PHRASING content, and an icon and a span are phrasing. The containment law
320
+ * forbids a BLOCK inside a button - a heading, a panel, another interactive
321
+ * node - and this was applying it to everything, so a real ImageUploader's two
322
+ * actions had their `Upload` and `Sparkles` glyphs hoisted out to become
323
+ * siblings: the icon left the button it belongs to (dono, 03/08).
324
+ *
325
+ * The reader upstream had already flattened them itself to obey the rule as
326
+ * written, which is why the census showed zero of these and the loss was
327
+ * invisible from here.
328
+ */
329
+ const phrasingKids = Array.isArray(node.children) &&
330
+ node.children.length > 0 &&
331
+ node.children.every((child) => PHRASING.has(String(child?.as ?? "")));
332
+ if ((CONTAINERS.has(as) || (PHRASING_HOST.has(as) && phrasingKids)) &&
333
+ Array.isArray(node.children)) {
306
334
  const children = walk(node.children, depth + 1);
307
335
  if (children.length > 0)
308
336
  node_.children = children;
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { scanTags } from "./css-modules.js";
22
22
  import { importMap, packageOfTag } from "./imports.js";
23
- import { definitionSpan } from "./transcribe.js";
23
+ import { definitionSpan, returnAt } from "./transcribe.js";
24
24
  /** `{children}` / `{props.children}` - the caller's content, spelled either way. */
25
25
  const CHILDREN = /\{\s*(?:props\.)?children\s*\}/;
26
26
  /**
@@ -182,6 +182,13 @@ export function sketchOf(source, name) {
182
182
  const span = name ? definitionSpan(source, name) : null;
183
183
  if (span)
184
184
  source = source.slice(span.from, span.to);
185
+ /**
186
+ * WHERE THE COMPONENT'S OWN RENDER STARTS, in the same slice the nodes are
187
+ * numbered against - so the index the sketch stamps and the index a reading sends
188
+ * are the same number.
189
+ */
190
+ const ownReturn = returnAt(source, name);
191
+ const rootAt = ownReturn === -1 ? -1 : source.indexOf("<", ownReturn);
185
192
  const events = scanTags(source);
186
193
  const out = [];
187
194
  let depth = 0;
@@ -194,6 +201,9 @@ export function sketchOf(source, name) {
194
201
  if (out.length >= MAX_NODES)
195
202
  break;
196
203
  const node = { tag: event.tag, depth };
204
+ // The first element at or after the component's own `return (` IS its root.
205
+ if (rootAt !== -1 && event.at === rootAt)
206
+ node.root = true;
197
207
  // What a capitalised tag IS, from the file's own imports - see `SketchNode.from`.
198
208
  if (/^[A-Z]/.test(event.tag)) {
199
209
  const pkg = packageOfTag(event.tag, imports);
@@ -1302,7 +1302,7 @@ function aliasTarget(source, name) {
1302
1302
  }
1303
1303
  return null;
1304
1304
  }
1305
- function returnAt(source, name) {
1305
+ export function returnAt(source, name) {
1306
1306
  if (name) {
1307
1307
  const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
1308
1308
  if (decl) {
@@ -681,7 +681,26 @@ external a library → needs "from": the package name
681
681
  slot the CALLER's content → takes "expects": what goes there, in their words
682
682
  \`\`\`
683
683
 
684
- Only \`row\` and \`stack\` take \`children\`. Everything else is a leaf.
684
+ \`row\` and \`stack\` ARRANGE their children. A \`component\` or \`external\` node HOLDS them -
685
+ what their code nests inside a component they compose is markup THEY wrote, wearing their
686
+ classes, and it belongs in the tree:
687
+
688
+ \`\`\`json
689
+ { "as": "component", "ref": "Card", "at": 3, "children": [
690
+ { "as": "row", "name": "header", "at": 4 },
691
+ { "as": "external", "from": "recharts", "at": 8 } ] }
692
+ \`\`\`
693
+
694
+ A \`button\` holds a glyph and its words - \`<button><Upload/>Upload</button>\` is valid HTML,
695
+ because a button's content model is *phrasing* and an icon is phrasing. So an \`icon\` and a
696
+ \`text\` may sit inside one:
697
+
698
+ \`\`\`json
699
+ { "as": "button", "name": "upload", "at": 9, "children": [{ "as": "icon", "name": "upload-icon", "at": 10 }] }
700
+ \`\`\`
701
+
702
+ Everything else is a leaf, and a leaf that holds elements has them HOISTED OUT to become
703
+ its siblings - which destroys the nesting you read. If something arranges, say \`row\`/\`stack\`.
685
704
 
686
705
  ### \`{children}\` is a node, not an absence
687
706
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.106",
3
+ "version": "0.16.107",
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": {