synthesisui 0.16.96 → 0.16.98

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.
@@ -96,10 +96,41 @@ function formOf(node) {
96
96
  * `declared` is their own tokens, so a `bg-ocean-500` on a nested part resolves
97
97
  * to the name they gave it rather than to a literal.
98
98
  */
99
+ /**
100
+ * WHAT THE PARENT PAINTS ON A FRONTIER, kept as written.
101
+ *
102
+ * A `component` or `external` node names another component and stops - and what
103
+ * the parent adds at the call site is the PARENT's decision, which had nowhere
104
+ * to go. Measured (not-expressed.md, dono, 01/08): `MetricCard` composes
105
+ * `<Card className="flex-1 dark:bg-darkgray-300">` and the dark override was
106
+ * lost, so the preview drew the wrong dark; `Paginate` lost a `min-w-[120px]`
107
+ * floor on both its buttons.
108
+ *
109
+ * Kept as the raw string rather than transcribed: this is an override on top of
110
+ * a recipe we do not own, and the renderer applies it as a class. Reading it into
111
+ * declarations would mean deciding which of the two wins, which is the compiler's
112
+ * job and not this module's.
113
+ */
114
+ function frontierClasses(node, sketch) {
115
+ const raw = typeof node.classes === "string"
116
+ ? node.classes
117
+ : typeof node.at === "number"
118
+ ? sketch?.[node.at]?.classes
119
+ : undefined;
120
+ const trimmed = raw?.trim();
121
+ return trimmed ? { classes: trimmed } : {};
122
+ }
99
123
  export function resolveAnatomy(read, declared, deps,
100
124
  /** Their name → the slug it reaches in this system. See `RefResolver`. */
101
125
  resolve,
102
- /** What the component returns, when the skill said and it is not a plain tag. */
126
+ /**
127
+ * What the component returns, when the skill said and it is not a plain tag.
128
+ *
129
+ * A STRING or an object: `{ name, at }` / `{ name, classes }` carries what the
130
+ * PARENT paints on the root at the call site. `MetricCard` returns
131
+ * `<Card className="flex-1 dark:bg-darkgray-300">` and that override had
132
+ * nowhere to travel (not-expressed.md, dono, 01/08).
133
+ */
103
134
  root,
104
135
  /** This component's sketch, so a node can name its classes by INDEX instead
105
136
  * of retyping them - see `AnatomyRead.at`. */
@@ -168,9 +199,12 @@ sketch) {
168
199
  * kebab, which is what a system generated by us would want anyway.
169
200
  */
170
201
  const slug = resolve?.(name) ?? safePartName(name);
171
- out.push(slug && slug !== safePartName(name)
172
- ? { as, ref: slug, refName: name }
173
- : { as, ref: slug || safePartName(name) });
202
+ out.push({
203
+ as,
204
+ ref: slug || safePartName(name),
205
+ ...(slug && slug !== safePartName(name) ? { refName: name } : {}),
206
+ ...frontierClasses(node, sketch),
207
+ });
174
208
  continue;
175
209
  }
176
210
  const from = String(node.from ?? "").trim();
@@ -179,7 +213,12 @@ sketch) {
179
213
  const version = deps?.[from];
180
214
  if (!external.some((e) => e.from === from))
181
215
  external.push({ from, version });
182
- out.push(version ? { as, from, version } : { as, from });
216
+ out.push({
217
+ as,
218
+ from,
219
+ ...(version ? { version } : {}),
220
+ ...frontierClasses(node, sketch),
221
+ });
183
222
  continue;
184
223
  }
185
224
  const node_ = { as };
@@ -258,15 +297,22 @@ sketch) {
258
297
  * through the crosswalk exactly like a child frontier does.
259
298
  */
260
299
  let rootOut;
261
- const rootRaw = String(root ?? "").trim();
300
+ const rootObj = typeof root === "object" && root ? root : null;
301
+ const rootRaw = String((rootObj ? rootObj.name : root) ?? "").trim();
302
+ // The parent's own override on the root it returns, by index or as written.
303
+ const rootPainted = rootObj
304
+ ? (frontierClasses(rootObj, sketch).classes ??
305
+ "")
306
+ : "";
307
+ const paints = rootPainted ? { classes: rootPainted } : {};
262
308
  if (rootRaw && /^[A-Z]/.test(rootRaw)) {
263
309
  if (rootRaw.includes(".")) {
264
- rootOut = { from: rootRaw };
310
+ rootOut = { from: rootRaw, ...paints };
265
311
  }
266
312
  else {
267
313
  const slug = resolve?.(rootRaw) ?? safePartName(rootRaw);
268
314
  if (slug)
269
- rootOut = { ref: slug, name: rootRaw };
315
+ rootOut = { ref: slug, name: rootRaw, ...paints };
270
316
  }
271
317
  }
272
318
  if (coerced > 0) {
@@ -409,11 +409,16 @@ export async function takeCensus(root, opts) {
409
409
  for (const extra of found.slice(1)) {
410
410
  const et = transcribe(rootClasses(src, extra.name), declaredValues);
411
411
  const etag = rootTag(src, extra.name);
412
+ // ITS OWN MARKUP, sliced at the definition boundary - the whole file's
413
+ // sketch belonged to the first component and the second read an empty
414
+ // one (not-expressed.md, dono, 01/08).
415
+ const esketch = sketchOf(src, extra.name);
412
416
  const esize = Object.keys(et.base).length +
413
417
  Object.keys(et.dark).length +
414
418
  Object.keys(et.states).length;
415
- if (esize > 0 || etag) {
419
+ if (esize > 0 || etag || esketch.length > 0) {
416
420
  looks[extra.name] = {
421
+ ...(esketch.length > 0 ? { sketch: esketch } : {}),
417
422
  ...et,
418
423
  ...(etag ? { rootTag: etag } : {}),
419
424
  };
@@ -510,7 +515,7 @@ export async function takeCensus(root, opts) {
510
515
  * and a file read whose content is here is a pipeline optimization failure -
511
516
  * the owner's rule, adopted verbatim (dono, 01/08).
512
517
  */
513
- const sketch = sketchOf(src);
518
+ const sketch = sketchOf(src, found[0].name);
514
519
  if (size > 0 || tag) {
515
520
  looks[found[0].name] = {
516
521
  ...(sketch.length > 0 ? { sketch } : {}),
@@ -368,10 +368,135 @@ async function validateRecipe(name, recipe) {
368
368
  * what would NOT survive. A list of 30 "this one is fine" lines is a wall the
369
369
  * reader has to skim to find the two that matter.
370
370
  */
371
- async function validateRecipes(entries) {
371
+ /**
372
+ * RESOLVE `at` BEFORE VALIDATING - the validator never sees the census.
373
+ *
374
+ * A node styled by `at` carries no `classes`, so it looks EMPTY to the validator
375
+ * and complete to the importer: four recipes came back BELOW THE FLOOR on a real
376
+ * run and all four were whole, because their pixels live in parts the sketch
377
+ * holds and the validator could not reach (not-expressed.md, dono, 01/08). The
378
+ * verdict was a false accusation about our own optimisation.
379
+ *
380
+ * This side HAS the census - the same file the lock and the sketch come from - so
381
+ * the index is resolved here, once, and the validator judges the recipe somebody
382
+ * is actually about to send. `classes` already present wins, exactly as the
383
+ * importer resolves it.
384
+ */
385
+ async function resolveAtNodes(root, entries) {
386
+ let looks = {};
387
+ try {
388
+ const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8");
389
+ looks = JSON.parse(raw).looks ?? {};
390
+ }
391
+ catch {
392
+ // No census: nothing to resolve, and validating the recipe as sent is still
393
+ // better than refusing to validate at all.
394
+ return entries;
395
+ }
396
+ /** Their spelling → the look, matched the way the contract kebabs a name. */
397
+ const lookFor = (name) => {
398
+ if (looks[name])
399
+ return looks[name];
400
+ const slug = name.replace(/[^a-z0-9]/gi, "").toLowerCase();
401
+ for (const [theirs, look] of Object.entries(looks)) {
402
+ if (theirs.replace(/[^a-z0-9]/gi, "").toLowerCase() === slug)
403
+ return look;
404
+ }
405
+ return undefined;
406
+ };
407
+ return entries.map((entry) => {
408
+ const look = lookFor(String(entry.name ?? ""));
409
+ const sketch = look?.sketch;
410
+ /**
411
+ * THE STYLES THE IMPORT WILL MERGE - so the floor measured is the floor the
412
+ * component arrives with.
413
+ *
414
+ * The reading sends an ANATOMY: a tree of `at` indices, deliberately without
415
+ * the classes, because retyping them is the loss the index exists to prevent.
416
+ * The styles are the census's measured `base` and `layers`, merged at import.
417
+ * So the validator was judging a recipe whose look had not arrived yet, and
418
+ * all 35 components came back BELOW THE FLOOR with the same four lines - a
419
+ * verdict about a payload nobody will ever send (not-expressed.md, dono,
420
+ * 01/08).
421
+ *
422
+ * Merged UNDER what the payload carries: a base the reader authored on
423
+ * purpose still wins over the measurement.
424
+ */
425
+ const recipe0 = entry.recipe;
426
+ const withLook = recipe0
427
+ ? {
428
+ ...recipe0,
429
+ ...(look?.base && Object.keys(look.base).length > 0
430
+ ? { base: { ...look.base, ...(recipe0.base ?? {}) } }
431
+ : {}),
432
+ ...(look?.layers?.length
433
+ ? {
434
+ layers: [
435
+ ...look.layers,
436
+ ...(recipe0.layers ?? []),
437
+ ],
438
+ }
439
+ : {}),
440
+ /**
441
+ * THE ROOT TAG TRAVELS, so the validator can name the KIND.
442
+ *
443
+ * It falls back to `surface` when the payload declares none, and every
444
+ * one of 35 components got the surface checklist - including a Button,
445
+ * two fields, a control and two pills (not-expressed.md, dono, 01/08).
446
+ * The kind is arithmetic from the name and the root tag; the tag is
447
+ * here and the arithmetic lives on the side that owns the contract, so
448
+ * this sends the evidence rather than a second implementation of it.
449
+ */
450
+ ...(look?.rootTag ? { rootTag: look.rootTag } : {}),
451
+ }
452
+ : recipe0;
453
+ entry = { ...entry, recipe: withLook };
454
+ if (!sketch)
455
+ return entry;
456
+ const walk = (nodes) => {
457
+ if (!Array.isArray(nodes))
458
+ return nodes;
459
+ return nodes.map((raw) => {
460
+ const node = raw;
461
+ const at = typeof node.at === "number" ? node.at : null;
462
+ const classes = typeof node.classes === "string"
463
+ ? node.classes
464
+ : at != null
465
+ ? sketch[at]?.classes
466
+ : undefined;
467
+ return {
468
+ ...node,
469
+ ...(classes ? { classes } : {}),
470
+ ...(node.children ? { children: walk(node.children) } : {}),
471
+ };
472
+ });
473
+ };
474
+ const recipe = entry.recipe;
475
+ if (!recipe)
476
+ return entry;
477
+ const anatomy = recipe.anatomy ??
478
+ recipe.preview?.parts;
479
+ if (!Array.isArray(anatomy))
480
+ return entry;
481
+ return {
482
+ ...entry,
483
+ recipe: recipe.anatomy
484
+ ? { ...recipe, anatomy: walk(recipe.anatomy) }
485
+ : {
486
+ ...recipe,
487
+ preview: {
488
+ ...recipe.preview,
489
+ parts: walk(recipe.preview.parts),
490
+ },
491
+ },
492
+ };
493
+ });
494
+ }
495
+ async function validateRecipes(root, entries) {
372
496
  if (entries.length === 0)
373
497
  return "Send at least one recipe: { recipes: [{ name, recipe }] }.";
374
- const answer = await askCatalogue("validate", { recipes: entries });
498
+ const resolved = await resolveAtNodes(root, entries);
499
+ const answer = await askCatalogue("validate", { recipes: resolved });
375
500
  if (!answer.ok)
376
501
  return answer.because;
377
502
  const body = answer.body;
@@ -662,10 +787,16 @@ async function callTool(root, name, args) {
662
787
  return text(await addComponent(root, String(args.name ?? "")));
663
788
  case "recipe_vocabulary":
664
789
  return text(await recipeVocabulary());
665
- case "validate_recipe":
666
- return text(await validateRecipe(String(args.name ?? "this component"), args.recipe));
790
+ case "validate_recipe": {
791
+ // Same resolution as the batch: an `at` the validator cannot see reads as
792
+ // an empty node and earns a false floor verdict (dono, 01/08).
793
+ const [only] = await resolveAtNodes(root, [
794
+ { name: String(args.name ?? "this component"), recipe: args.recipe },
795
+ ]);
796
+ return text(await validateRecipe(String(args.name ?? "this component"), only?.recipe ?? args.recipe));
797
+ }
667
798
  case "validate_recipes":
668
- return text(await validateRecipes(Array.isArray(args.recipes)
799
+ return text(await validateRecipes(root, Array.isArray(args.recipes)
669
800
  ? args.recipes
670
801
  : []));
671
802
  case "request_component": {
@@ -19,6 +19,7 @@
19
19
  * same in an atomic library, a feature-foldered app, or a single flat directory.
20
20
  */
21
21
  import { scanTags } from "./css-modules.js";
22
+ import { definitionSpan } from "./transcribe.js";
22
23
  /**
23
24
  * Past this a component is a page in disguise, and the gate already said no.
24
25
  *
@@ -96,7 +97,22 @@ function staticCallClasses(body) {
96
97
  * sitting directly between an open and its close - dynamic children are the caller's,
97
98
  * and the slot form already says so.
98
99
  */
99
- export function sketchOf(source) {
100
+ export function sketchOf(source, name) {
101
+ /**
102
+ * ONE COMPONENT'S MARKUP, when the file declares more than one.
103
+ *
104
+ * `RadioCard.tsx` declares `RadioCard` and `RadioCardGroup`, and this used to
105
+ * hand all sixteen nodes to whoever asked first - node 15 being
106
+ * `<RadioGroup className="flex flex-col gap-3">`, the OTHER component's root.
107
+ * So one component read a sketch with a stranger's markup in it and the other
108
+ * read an empty one (not-expressed.md, dono, 01/08).
109
+ *
110
+ * A name that is not in the file, or one whose body has no braces, reads the
111
+ * whole file - which is right for the 34 files that declare one component.
112
+ */
113
+ const span = name ? definitionSpan(source, name) : null;
114
+ if (span)
115
+ source = source.slice(span.from, span.to);
100
116
  const events = scanTags(source);
101
117
  const out = [];
102
118
  let depth = 0;
@@ -146,6 +146,34 @@ const SIZE_KEYWORD = {
146
146
  px: "1px",
147
147
  "0": "0",
148
148
  };
149
+ /**
150
+ * `max-w-*` HAS ITS OWN RAMP - and it is not the spacing scale.
151
+ *
152
+ * Tailwind gives max-width a named ladder of its own (`xs` 20rem through `7xl`
153
+ * 80rem, plus `none` and `prose`), and the reader only knew the spacing scale
154
+ * and the keyword sizes - so `max-w-2xl` resolved to nothing and was reported as
155
+ * a class that "starts like a design decision and resolves to nothing"
156
+ * (not-expressed.md, dono, 01/08). It resolves to 42rem, and the class was fine;
157
+ * the table was short.
158
+ *
159
+ * Scoped to `max-w`/`max-h` on purpose: `w-2xl` is not a utility, and `p-2xl`
160
+ * means the spacing scale. A ramp applied on the wrong axis would invent a width.
161
+ */
162
+ const MAX_WIDTH = {
163
+ none: "none",
164
+ xs: "20rem",
165
+ sm: "24rem",
166
+ md: "28rem",
167
+ lg: "32rem",
168
+ xl: "36rem",
169
+ "2xl": "42rem",
170
+ "3xl": "48rem",
171
+ "4xl": "56rem",
172
+ "5xl": "64rem",
173
+ "6xl": "72rem",
174
+ "7xl": "80rem",
175
+ prose: "65ch",
176
+ };
149
177
  /** Axis-dependent: `w-screen` is 100vw and `h-screen` is 100vh. */
150
178
  const SCREEN = {
151
179
  w: "100vw",
@@ -635,6 +663,10 @@ export function readUtility(utility, declared) {
635
663
  /^(w|h|min-w|min-h|max-w|max-h|size)$/.test(sizePrefix)) {
636
664
  return { property: spaceProp, value: SIZE_KEYWORD[sizeRest] };
637
665
  }
666
+ /** `max-w-2xl` - max-width's own named ramp, on its own axis only. */
667
+ if (MAX_WIDTH[sizeRest] && /^(max-w|max-h)$/.test(sizePrefix)) {
668
+ return { property: spaceProp, value: MAX_WIDTH[sizeRest] };
669
+ }
638
670
  }
639
671
  if (prefix === "rounded") {
640
672
  /**
@@ -1103,18 +1135,234 @@ export function transcribe(classes, declared) {
1103
1135
  * Absent or unfound, the old behaviour stands exactly - which keeps every
1104
1136
  * single-component file reading byte-identically.
1105
1137
  */
1138
+ /**
1139
+ * AN EXPORT THAT IS AN ALIAS - follow it to the function it wraps.
1140
+ *
1141
+ * `export const Chat = Object.assign(React.forwardRef(ChatContainerComponent), …)`
1142
+ * declares the NAME the census knows and none of the markup: the styles live in
1143
+ * `ChatContainerComponent`, declared above. Measured on a real library, EIGHT of
1144
+ * 36 exports are this shape - seven `forwardRef(Fn)` and one
1145
+ * `Object.assign(forwardRef(Fn))` - and Chat arrived with an empty look, which
1146
+ * forced the one hand-read of the whole run (not-expressed.md, dono, 01/08).
1147
+ *
1148
+ * They used to work by accident: with no body brace to find, the reader fell
1149
+ * back to the first `return (` in the file, which for a one-component file is
1150
+ * the right one. Removing that fallback - correctly, because it let a helper
1151
+ * borrow a sibling's return - would have broken all eight. So the alias is
1152
+ * followed on purpose instead: the first capitalised identifier inside the call
1153
+ * is the function this name IS.
1154
+ */
1155
+ function aliasTarget(source, name) {
1156
+ const decl = new RegExp(`(?:const|let|var)\\s+${name}\\s*(?::[^=]*)?=\\s*([^;\\n]{0,200})`).exec(source);
1157
+ if (!decl)
1158
+ return null;
1159
+ const rhs = decl[1];
1160
+ // Only a WRAPPER call aliases: `forwardRef`, `memo`, `Object.assign`,
1161
+ // `styled(...)`. A plain arrow is the component itself and has no target.
1162
+ if (!/^(?:React\.)?(?:forwardRef|memo|Object\.assign|styled)\b/.test(rhs))
1163
+ return null;
1164
+ for (const m of rhs.matchAll(/\b([A-Z][A-Za-z0-9_]*)\b/g)) {
1165
+ const candidate = m[1];
1166
+ if (/^(React|Object|Component)$/.test(candidate))
1167
+ continue;
1168
+ if (candidate === name)
1169
+ continue;
1170
+ // It has to be declared in this file, or it is a type argument or an import.
1171
+ if (new RegExp(`(?:function|const|let|var)\\s+${candidate}\\b`).test(source))
1172
+ return candidate;
1173
+ }
1174
+ return null;
1175
+ }
1106
1176
  function returnAt(source, name) {
1107
1177
  if (name) {
1108
1178
  const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
1109
1179
  if (decl) {
1110
- const from = source.slice(decl.index);
1111
- const ret = from.search(/return\s*\(/);
1112
- if (ret !== -1)
1113
- return decl.index + ret;
1180
+ const own = ownReturn(source, decl.index);
1181
+ if (own !== -1)
1182
+ return own;
1183
+ /**
1184
+ * A BRACE-LESS ARROW returns its JSX straight after `=>`, so there is no
1185
+ * body and no `return` to find - `const X = () => (<div/>)`. That is a
1186
+ * real shape and it gets the concise position.
1187
+ */
1188
+ const arrow = /=>\s*\(/.exec(source.slice(decl.index, decl.index + 400));
1189
+ if (arrow)
1190
+ return decl.index + arrow.index;
1191
+ /**
1192
+ * AN ALIAS POINTS AT THE FUNCTION THAT HAS THE MARKUP - follow it once.
1193
+ * `const Chat = Object.assign(forwardRef(ChatContainerComponent), …)`.
1194
+ */
1195
+ const target = aliasTarget(source, name);
1196
+ if (target) {
1197
+ const viaAlias = ownReturn(source, source.search(new RegExp(`(?:function|const|let|var)\\s+${target}\\b`)));
1198
+ if (viaAlias !== -1)
1199
+ return viaAlias;
1200
+ }
1201
+ /**
1202
+ * FOUND THE NAME AND IT RETURNS NO JSX IN PARENTHESES - so say nothing.
1203
+ *
1204
+ * The lexical fallback used to run here and it borrowed the NEXT
1205
+ * component's return: a `Helper` that returns a number handed back a
1206
+ * sibling's `<div className="p-2">`. That is the same defect the charts
1207
+ * had, one shape along - and my own spec caught it (dono, 01/08).
1208
+ */
1209
+ return -1;
1114
1210
  }
1115
1211
  }
1116
1212
  return source.search(/return\s*\(/);
1117
1213
  }
1214
+ /**
1215
+ * THE COMPONENT'S OWN `return (`, not the first one after its name.
1216
+ *
1217
+ * Scoping by name was half the answer and the charts proved it: `LineChart` is
1218
+ * declared on line 99, and the first `return (` after that is on line 158 -
1219
+ * inside `CustomTooltip`, a closure NESTED IN ITS BODY. So all three charts
1220
+ * arrived with the tooltip's surface as their base: `bg-white/90 backdrop-blur-md
1221
+ * p-3 rounded-xl shadow-2xl` on what should be a card (not-expressed.md, dono,
1222
+ * 01/08).
1223
+ *
1224
+ * A function's own return sits at the body's OWN brace depth. So: walk from the
1225
+ * declaration, find the body's opening brace, then take the first `return (`
1226
+ * that appears while depth is exactly one - anything deeper belongs to a closure
1227
+ * this component happens to contain.
1228
+ *
1229
+ * Quote-aware, because a brace inside a string is not a brace. An arrow body
1230
+ * with no braces at all (`const X = () => (<div/>)`) has no depth to track and
1231
+ * falls through to the caller's lexical search, which is right for it.
1232
+ */
1233
+ /**
1234
+ * WHERE ONE COMPONENT'S CODE STARTS AND STOPS, by name.
1235
+ *
1236
+ * A file that declares two components used to hand both bodies to whatever read
1237
+ * it first: `RadioCard.tsx` declares `RadioCard` and `RadioCardGroup`, and the
1238
+ * sketch attributed all sixteen nodes to `RadioCard` - node 15 being
1239
+ * `<RadioGroup className="flex flex-col gap-3">`, which is the OTHER component's
1240
+ * root (not-expressed.md, dono, 01/08).
1241
+ *
1242
+ * Returns null when the name is not in the file or has no braced body, and the
1243
+ * caller then reads the whole file - which is right for the 34 files that
1244
+ * declare one component.
1245
+ */
1246
+ export function definitionSpan(source, name) {
1247
+ const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
1248
+ if (!decl)
1249
+ return null;
1250
+ const brace = bodyBrace(source, decl.index);
1251
+ if (brace === -1) {
1252
+ // The export is an alias; the body belongs to what it wraps.
1253
+ const target = aliasTarget(source, name);
1254
+ if (target)
1255
+ return definitionSpan(source, target);
1256
+ return null;
1257
+ }
1258
+ const end = matchingBrace(source, brace);
1259
+ return { from: decl.index, to: end === -1 ? source.length : end + 1 };
1260
+ }
1261
+ /** The `{` that opens a function body declared at `declAt`, or -1. */
1262
+ function bodyBrace(source, declAt) {
1263
+ /**
1264
+ * A BODY INLINE INSIDE A WRAPPER: `forwardRef<A, B>((props, ref) => { … })`.
1265
+ *
1266
+ * Toggle is written this way and the plain walk below cannot find it - the
1267
+ * wrapper's own parenthesis never closes before the body starts, so it looked
1268
+ * for a `{` that is not there and the component read empty. There is no named
1269
+ * function to alias to either: the body IS the argument.
1270
+ */
1271
+ const head = source.slice(declAt, declAt + 400);
1272
+ const wrapper = /=\s*(?:React\.)?(?:forwardRef|memo|styled\([^)]*\))\s*(?:<[^>(]*>)?\s*\(/.exec(head);
1273
+ if (wrapper) {
1274
+ const arrow = /(?:=>|function\s*[A-Za-z0-9_]*\s*\([^)]*\))\s*\{/.exec(source.slice(declAt + wrapper.index));
1275
+ if (arrow) {
1276
+ const at = declAt + wrapper.index + arrow.index + arrow[0].length - 1;
1277
+ if (source[at] === "{")
1278
+ return at;
1279
+ }
1280
+ }
1281
+ let i = declAt;
1282
+ let paren = 0;
1283
+ let opened = false;
1284
+ for (; i < source.length; i++) {
1285
+ const ch = source[i];
1286
+ if (ch === "(") {
1287
+ paren += 1;
1288
+ opened = true;
1289
+ }
1290
+ else if (ch === ")") {
1291
+ paren -= 1;
1292
+ if (opened && paren <= 0) {
1293
+ i += 1;
1294
+ break;
1295
+ }
1296
+ }
1297
+ else if (ch === "{" && !opened) {
1298
+ return -1; // `const X = { … }` is an object, not a body
1299
+ }
1300
+ }
1301
+ return source.indexOf("{", i);
1302
+ }
1303
+ /** The `}` that closes the brace at `open`, quote-aware. */
1304
+ function matchingBrace(source, open) {
1305
+ let depth = 0;
1306
+ let quote = null;
1307
+ for (let j = open; j < source.length; j++) {
1308
+ const ch = source[j];
1309
+ if (quote) {
1310
+ if (ch === "\\")
1311
+ j += 1;
1312
+ else if (ch === quote)
1313
+ quote = null;
1314
+ continue;
1315
+ }
1316
+ if (ch === '"' || ch === "'" || ch === "`") {
1317
+ quote = ch;
1318
+ continue;
1319
+ }
1320
+ if (ch === "{")
1321
+ depth += 1;
1322
+ else if (ch === "}") {
1323
+ depth -= 1;
1324
+ if (depth === 0)
1325
+ return j;
1326
+ }
1327
+ }
1328
+ return -1;
1329
+ }
1330
+ function ownReturn(source, declAt) {
1331
+ const brace = bodyBrace(source, declAt);
1332
+ if (brace === -1)
1333
+ return -1;
1334
+ let depth = 0;
1335
+ let quote = null;
1336
+ for (let j = brace; j < source.length; j++) {
1337
+ const ch = source[j];
1338
+ if (quote) {
1339
+ if (ch === "\\")
1340
+ j += 1;
1341
+ else if (ch === quote)
1342
+ quote = null;
1343
+ continue;
1344
+ }
1345
+ if (ch === '"' || ch === "'" || ch === "`") {
1346
+ quote = ch;
1347
+ continue;
1348
+ }
1349
+ if (ch === "{")
1350
+ depth += 1;
1351
+ else if (ch === "}") {
1352
+ depth -= 1;
1353
+ // Out of the body without finding one: this component returns no JSX in
1354
+ // parentheses, and guessing from a sibling would be worse than nothing.
1355
+ if (depth === 0)
1356
+ return -1;
1357
+ }
1358
+ else if (depth === 1 &&
1359
+ ch === "r" &&
1360
+ /^return\s*\(/.test(source.slice(j))) {
1361
+ return j;
1362
+ }
1363
+ }
1364
+ return -1;
1365
+ }
1118
1366
  export function rootTag(source, name) {
1119
1367
  const ret = returnAt(source, name);
1120
1368
  if (ret === -1)
@@ -614,10 +614,21 @@ depth appears anyway.
614
614
  "Modal": {
615
615
  "root": "BaseDialog.Root",
616
616
  "anatomy": [ … ]
617
+ },
618
+ "MetricCard": {
619
+ "root": { "name": "Card", "at": 1 },
620
+ "anatomy": [ … ]
617
621
  }
618
622
  }
619
623
  \`\`\`
620
624
 
625
+ **The root takes what the PARENT paints on it.** When the returned root carries an override at
626
+ the call site - \`MetricCard\` returns \`<Card className="flex-1 dark:bg-darkgray-300">\` - send the
627
+ object form and point \`at\` the sketch node: \`{ "name": "Card", "at": 1 }\`. The string form still
628
+ works and means "no override". Sending the root as an anatomy node instead would make it a leaf
629
+ and orphan the content inside it, which is why it is a field on the root rather than a node
630
+ (not-expressed.md, dono, 01/08).
631
+
621
632
  **Say what the component RETURNS when it is not a plain tag.** Measured on a real library
622
633
  (dono, 01/08): 14 of 23 components return one of their own - \`ArticleCard\` returns a \`<Card>\`,
623
634
  \`Button\` and \`Text\` return a \`<Component>\` - and 17 of 23 have no style of their own at all,
@@ -711,6 +722,21 @@ of. It previews as a block carrying its name, and it becomes a **rule** - \`Arti
711
722
  That edge is a **fact, not a habit**: you saw it in the definition, so it is true once and for
712
723
  all, and it arrives active without waiting for a third sighting.
713
724
 
725
+ **Send what the PARENT paints on it.** A frontier node takes \`at\` (or \`classes\`) like any other,
726
+ and it means something different there: not the child's look, which has a recipe of its own, but
727
+ the override the parent applies where it composes the child. Measured on a real library, four of
728
+ these were being lost:
729
+
730
+ \`\`\`
731
+ Paginate <Button className="min-w-[120px] dark:bg-ocean-800 …"> a 120px floor
732
+ MetricCard <Card className="flex-1 dark:bg-darkgray-300"> overrides Card's dark
733
+ ArticleCard <Tag className="w-fit">
734
+ Message <ReactMarkdown className="text-royal-blue-500 underline …">
735
+ \`\`\`
736
+
737
+ The MetricCard one is the sharpest: losing that override means the preview draws the wrong dark.
738
+ Same for an \`external\` node - a library's component still wears what their code puts on it.
739
+
714
740
  Three things that are NOT edges, and sending them as such would be wrong:
715
741
 
716
742
  - \`motion.div\`, \`Dialog.Root\`, \`Radio.Item\` - a library's namespace, not a component of theirs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.96",
3
+ "version": "0.16.98",
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": {