synthesisui 0.16.96 → 0.16.97
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-read.js +36 -4
- package/dist/commands/import.js +7 -2
- package/dist/commands/mcp.js +91 -5
- package/dist/doctor/sketch.js +17 -1
- package/dist/doctor/transcribe.js +181 -4
- package/dist/skill-import.js +15 -0
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -96,6 +96,30 @@ 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,
|
|
@@ -168,9 +192,12 @@ sketch) {
|
|
|
168
192
|
* kebab, which is what a system generated by us would want anyway.
|
|
169
193
|
*/
|
|
170
194
|
const slug = resolve?.(name) ?? safePartName(name);
|
|
171
|
-
out.push(
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
out.push({
|
|
196
|
+
as,
|
|
197
|
+
ref: slug || safePartName(name),
|
|
198
|
+
...(slug && slug !== safePartName(name) ? { refName: name } : {}),
|
|
199
|
+
...frontierClasses(node, sketch),
|
|
200
|
+
});
|
|
174
201
|
continue;
|
|
175
202
|
}
|
|
176
203
|
const from = String(node.from ?? "").trim();
|
|
@@ -179,7 +206,12 @@ sketch) {
|
|
|
179
206
|
const version = deps?.[from];
|
|
180
207
|
if (!external.some((e) => e.from === from))
|
|
181
208
|
external.push({ from, version });
|
|
182
|
-
out.push(
|
|
209
|
+
out.push({
|
|
210
|
+
as,
|
|
211
|
+
from,
|
|
212
|
+
...(version ? { version } : {}),
|
|
213
|
+
...frontierClasses(node, sketch),
|
|
214
|
+
});
|
|
183
215
|
continue;
|
|
184
216
|
}
|
|
185
217
|
const node_ = { as };
|
package/dist/commands/import.js
CHANGED
|
@@ -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 } : {}),
|
package/dist/commands/mcp.js
CHANGED
|
@@ -368,10 +368,90 @@ 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
|
-
|
|
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 sketch, matched the way the contract kebabs a name. */
|
|
397
|
+
const sketchFor = (name) => {
|
|
398
|
+
if (looks[name]?.sketch)
|
|
399
|
+
return looks[name].sketch;
|
|
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.sketch;
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
};
|
|
407
|
+
return entries.map((entry) => {
|
|
408
|
+
const sketch = sketchFor(String(entry.name ?? ""));
|
|
409
|
+
if (!sketch)
|
|
410
|
+
return entry;
|
|
411
|
+
const walk = (nodes) => {
|
|
412
|
+
if (!Array.isArray(nodes))
|
|
413
|
+
return nodes;
|
|
414
|
+
return nodes.map((raw) => {
|
|
415
|
+
const node = raw;
|
|
416
|
+
const at = typeof node.at === "number" ? node.at : null;
|
|
417
|
+
const classes = typeof node.classes === "string"
|
|
418
|
+
? node.classes
|
|
419
|
+
: at != null
|
|
420
|
+
? sketch[at]?.classes
|
|
421
|
+
: undefined;
|
|
422
|
+
return {
|
|
423
|
+
...node,
|
|
424
|
+
...(classes ? { classes } : {}),
|
|
425
|
+
...(node.children ? { children: walk(node.children) } : {}),
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
const recipe = entry.recipe;
|
|
430
|
+
if (!recipe)
|
|
431
|
+
return entry;
|
|
432
|
+
const anatomy = recipe.anatomy ??
|
|
433
|
+
recipe.preview?.parts;
|
|
434
|
+
if (!Array.isArray(anatomy))
|
|
435
|
+
return entry;
|
|
436
|
+
return {
|
|
437
|
+
...entry,
|
|
438
|
+
recipe: recipe.anatomy
|
|
439
|
+
? { ...recipe, anatomy: walk(recipe.anatomy) }
|
|
440
|
+
: {
|
|
441
|
+
...recipe,
|
|
442
|
+
preview: {
|
|
443
|
+
...recipe.preview,
|
|
444
|
+
parts: walk(recipe.preview.parts),
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
async function validateRecipes(root, entries) {
|
|
372
451
|
if (entries.length === 0)
|
|
373
452
|
return "Send at least one recipe: { recipes: [{ name, recipe }] }.";
|
|
374
|
-
const
|
|
453
|
+
const resolved = await resolveAtNodes(root, entries);
|
|
454
|
+
const answer = await askCatalogue("validate", { recipes: resolved });
|
|
375
455
|
if (!answer.ok)
|
|
376
456
|
return answer.because;
|
|
377
457
|
const body = answer.body;
|
|
@@ -662,10 +742,16 @@ async function callTool(root, name, args) {
|
|
|
662
742
|
return text(await addComponent(root, String(args.name ?? "")));
|
|
663
743
|
case "recipe_vocabulary":
|
|
664
744
|
return text(await recipeVocabulary());
|
|
665
|
-
case "validate_recipe":
|
|
666
|
-
|
|
745
|
+
case "validate_recipe": {
|
|
746
|
+
// Same resolution as the batch: an `at` the validator cannot see reads as
|
|
747
|
+
// an empty node and earns a false floor verdict (dono, 01/08).
|
|
748
|
+
const [only] = await resolveAtNodes(root, [
|
|
749
|
+
{ name: String(args.name ?? "this component"), recipe: args.recipe },
|
|
750
|
+
]);
|
|
751
|
+
return text(await validateRecipe(String(args.name ?? "this component"), only?.recipe ?? args.recipe));
|
|
752
|
+
}
|
|
667
753
|
case "validate_recipes":
|
|
668
|
-
return text(await validateRecipes(Array.isArray(args.recipes)
|
|
754
|
+
return text(await validateRecipes(root, Array.isArray(args.recipes)
|
|
669
755
|
? args.recipes
|
|
670
756
|
: []));
|
|
671
757
|
case "request_component": {
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -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
|
/**
|
|
@@ -1107,14 +1139,159 @@ function returnAt(source, name) {
|
|
|
1107
1139
|
if (name) {
|
|
1108
1140
|
const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
|
|
1109
1141
|
if (decl) {
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1142
|
+
const own = ownReturn(source, decl.index);
|
|
1143
|
+
if (own !== -1)
|
|
1144
|
+
return own;
|
|
1145
|
+
/**
|
|
1146
|
+
* A BRACE-LESS ARROW returns its JSX straight after `=>`, so there is no
|
|
1147
|
+
* body and no `return` to find - `const X = () => (<div/>)`. That is a
|
|
1148
|
+
* real shape and it gets the concise position.
|
|
1149
|
+
*/
|
|
1150
|
+
const arrow = /=>\s*\(/.exec(source.slice(decl.index, decl.index + 400));
|
|
1151
|
+
if (arrow)
|
|
1152
|
+
return decl.index + arrow.index;
|
|
1153
|
+
/**
|
|
1154
|
+
* FOUND THE NAME AND IT RETURNS NO JSX IN PARENTHESES - so say nothing.
|
|
1155
|
+
*
|
|
1156
|
+
* The lexical fallback used to run here and it borrowed the NEXT
|
|
1157
|
+
* component's return: a `Helper` that returns a number handed back a
|
|
1158
|
+
* sibling's `<div className="p-2">`. That is the same defect the charts
|
|
1159
|
+
* had, one shape along - and my own spec caught it (dono, 01/08).
|
|
1160
|
+
*/
|
|
1161
|
+
return -1;
|
|
1114
1162
|
}
|
|
1115
1163
|
}
|
|
1116
1164
|
return source.search(/return\s*\(/);
|
|
1117
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* THE COMPONENT'S OWN `return (`, not the first one after its name.
|
|
1168
|
+
*
|
|
1169
|
+
* Scoping by name was half the answer and the charts proved it: `LineChart` is
|
|
1170
|
+
* declared on line 99, and the first `return (` after that is on line 158 -
|
|
1171
|
+
* inside `CustomTooltip`, a closure NESTED IN ITS BODY. So all three charts
|
|
1172
|
+
* arrived with the tooltip's surface as their base: `bg-white/90 backdrop-blur-md
|
|
1173
|
+
* p-3 rounded-xl shadow-2xl` on what should be a card (not-expressed.md, dono,
|
|
1174
|
+
* 01/08).
|
|
1175
|
+
*
|
|
1176
|
+
* A function's own return sits at the body's OWN brace depth. So: walk from the
|
|
1177
|
+
* declaration, find the body's opening brace, then take the first `return (`
|
|
1178
|
+
* that appears while depth is exactly one - anything deeper belongs to a closure
|
|
1179
|
+
* this component happens to contain.
|
|
1180
|
+
*
|
|
1181
|
+
* Quote-aware, because a brace inside a string is not a brace. An arrow body
|
|
1182
|
+
* with no braces at all (`const X = () => (<div/>)`) has no depth to track and
|
|
1183
|
+
* falls through to the caller's lexical search, which is right for it.
|
|
1184
|
+
*/
|
|
1185
|
+
/**
|
|
1186
|
+
* WHERE ONE COMPONENT'S CODE STARTS AND STOPS, by name.
|
|
1187
|
+
*
|
|
1188
|
+
* A file that declares two components used to hand both bodies to whatever read
|
|
1189
|
+
* it first: `RadioCard.tsx` declares `RadioCard` and `RadioCardGroup`, and the
|
|
1190
|
+
* sketch attributed all sixteen nodes to `RadioCard` - node 15 being
|
|
1191
|
+
* `<RadioGroup className="flex flex-col gap-3">`, which is the OTHER component's
|
|
1192
|
+
* root (not-expressed.md, dono, 01/08).
|
|
1193
|
+
*
|
|
1194
|
+
* Returns null when the name is not in the file or has no braced body, and the
|
|
1195
|
+
* caller then reads the whole file - which is right for the 34 files that
|
|
1196
|
+
* declare one component.
|
|
1197
|
+
*/
|
|
1198
|
+
export function definitionSpan(source, name) {
|
|
1199
|
+
const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
|
|
1200
|
+
if (!decl)
|
|
1201
|
+
return null;
|
|
1202
|
+
const brace = bodyBrace(source, decl.index);
|
|
1203
|
+
if (brace === -1)
|
|
1204
|
+
return null;
|
|
1205
|
+
const end = matchingBrace(source, brace);
|
|
1206
|
+
return { from: decl.index, to: end === -1 ? source.length : end + 1 };
|
|
1207
|
+
}
|
|
1208
|
+
/** The `{` that opens a function body declared at `declAt`, or -1. */
|
|
1209
|
+
function bodyBrace(source, declAt) {
|
|
1210
|
+
let i = declAt;
|
|
1211
|
+
let paren = 0;
|
|
1212
|
+
let opened = false;
|
|
1213
|
+
for (; i < source.length; i++) {
|
|
1214
|
+
const ch = source[i];
|
|
1215
|
+
if (ch === "(") {
|
|
1216
|
+
paren += 1;
|
|
1217
|
+
opened = true;
|
|
1218
|
+
}
|
|
1219
|
+
else if (ch === ")") {
|
|
1220
|
+
paren -= 1;
|
|
1221
|
+
if (opened && paren <= 0) {
|
|
1222
|
+
i += 1;
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
else if (ch === "{" && !opened) {
|
|
1227
|
+
return -1; // `const X = { … }` is an object, not a body
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return source.indexOf("{", i);
|
|
1231
|
+
}
|
|
1232
|
+
/** The `}` that closes the brace at `open`, quote-aware. */
|
|
1233
|
+
function matchingBrace(source, open) {
|
|
1234
|
+
let depth = 0;
|
|
1235
|
+
let quote = null;
|
|
1236
|
+
for (let j = open; j < source.length; j++) {
|
|
1237
|
+
const ch = source[j];
|
|
1238
|
+
if (quote) {
|
|
1239
|
+
if (ch === "\\")
|
|
1240
|
+
j += 1;
|
|
1241
|
+
else if (ch === quote)
|
|
1242
|
+
quote = null;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1246
|
+
quote = ch;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (ch === "{")
|
|
1250
|
+
depth += 1;
|
|
1251
|
+
else if (ch === "}") {
|
|
1252
|
+
depth -= 1;
|
|
1253
|
+
if (depth === 0)
|
|
1254
|
+
return j;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return -1;
|
|
1258
|
+
}
|
|
1259
|
+
function ownReturn(source, declAt) {
|
|
1260
|
+
const brace = bodyBrace(source, declAt);
|
|
1261
|
+
if (brace === -1)
|
|
1262
|
+
return -1;
|
|
1263
|
+
let depth = 0;
|
|
1264
|
+
let quote = null;
|
|
1265
|
+
for (let j = brace; j < source.length; j++) {
|
|
1266
|
+
const ch = source[j];
|
|
1267
|
+
if (quote) {
|
|
1268
|
+
if (ch === "\\")
|
|
1269
|
+
j += 1;
|
|
1270
|
+
else if (ch === quote)
|
|
1271
|
+
quote = null;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1275
|
+
quote = ch;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (ch === "{")
|
|
1279
|
+
depth += 1;
|
|
1280
|
+
else if (ch === "}") {
|
|
1281
|
+
depth -= 1;
|
|
1282
|
+
// Out of the body without finding one: this component returns no JSX in
|
|
1283
|
+
// parentheses, and guessing from a sibling would be worse than nothing.
|
|
1284
|
+
if (depth === 0)
|
|
1285
|
+
return -1;
|
|
1286
|
+
}
|
|
1287
|
+
else if (depth === 1 &&
|
|
1288
|
+
ch === "r" &&
|
|
1289
|
+
/^return\s*\(/.test(source.slice(j))) {
|
|
1290
|
+
return j;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return -1;
|
|
1294
|
+
}
|
|
1118
1295
|
export function rootTag(source, name) {
|
|
1119
1296
|
const ret = returnAt(source, name);
|
|
1120
1297
|
if (ret === -1)
|
package/dist/skill-import.js
CHANGED
|
@@ -711,6 +711,21 @@ of. It previews as a block carrying its name, and it becomes a **rule** - \`Arti
|
|
|
711
711
|
That edge is a **fact, not a habit**: you saw it in the definition, so it is true once and for
|
|
712
712
|
all, and it arrives active without waiting for a third sighting.
|
|
713
713
|
|
|
714
|
+
**Send what the PARENT paints on it.** A frontier node takes \`at\` (or \`classes\`) like any other,
|
|
715
|
+
and it means something different there: not the child's look, which has a recipe of its own, but
|
|
716
|
+
the override the parent applies where it composes the child. Measured on a real library, four of
|
|
717
|
+
these were being lost:
|
|
718
|
+
|
|
719
|
+
\`\`\`
|
|
720
|
+
Paginate <Button className="min-w-[120px] dark:bg-ocean-800 …"> a 120px floor
|
|
721
|
+
MetricCard <Card className="flex-1 dark:bg-darkgray-300"> overrides Card's dark
|
|
722
|
+
ArticleCard <Tag className="w-fit">
|
|
723
|
+
Message <ReactMarkdown className="text-royal-blue-500 underline …">
|
|
724
|
+
\`\`\`
|
|
725
|
+
|
|
726
|
+
The MetricCard one is the sharpest: losing that override means the preview draws the wrong dark.
|
|
727
|
+
Same for an \`external\` node - a library's component still wears what their code puts on it.
|
|
728
|
+
|
|
714
729
|
Three things that are NOT edges, and sending them as such would be wrong:
|
|
715
730
|
|
|
716
731
|
- \`motion.div\`, \`Dialog.Root\`, \`Radio.Item\` - a library's namespace, not a component of theirs
|
package/package.json
CHANGED