synthesisui 0.16.79 → 0.16.80
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 +15 -1
- package/dist/commands/import.js +190 -12
- package/dist/commands/mcp.js +9 -0
- package/dist/component-codegen.js +11 -0
- package/dist/doctor/components-scan.js +70 -2
- package/dist/doctor/crosswalk.js +59 -2
- package/dist/doctor/definitions-scan.js +32 -0
- package/dist/index.js +35 -5
- package/dist/skill-import.js +107 -11
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* of the three outcomes this pipeline has paid for twice.
|
|
31
31
|
*/
|
|
32
32
|
import { transcribe, } from "./doctor/transcribe.js";
|
|
33
|
-
/** The
|
|
33
|
+
/** The ten forms, closed. A renderer that accepts any tag draws anything. */
|
|
34
34
|
const FORMS = new Set([
|
|
35
35
|
"image",
|
|
36
36
|
"heading",
|
|
@@ -42,6 +42,7 @@ const FORMS = new Set([
|
|
|
42
42
|
"stack",
|
|
43
43
|
"component",
|
|
44
44
|
"external",
|
|
45
|
+
"slot",
|
|
45
46
|
]);
|
|
46
47
|
/** The two that arrange, and the only two that take children. */
|
|
47
48
|
const CONTAINERS = new Set(["row", "stack"]);
|
|
@@ -198,6 +199,19 @@ root) {
|
|
|
198
199
|
const text = typeof node.text === "string" ? node.text.trim() : "";
|
|
199
200
|
if (text)
|
|
200
201
|
node_.text = text.slice(0, 120);
|
|
202
|
+
/**
|
|
203
|
+
* A SLOT KEEPS ITS STYLES AND BORROWS ITS CONTENT.
|
|
204
|
+
*
|
|
205
|
+
* Unlike the other two frontiers it is not somebody else's element - it IS
|
|
206
|
+
* theirs, and its border and hover belong on it. Only what goes inside is the
|
|
207
|
+
* caller's, so `expects` says what that is in their words instead of the
|
|
208
|
+
* preview drawing a blank.
|
|
209
|
+
*/
|
|
210
|
+
if (as === "slot") {
|
|
211
|
+
const expects = typeof node.expects === "string" ? node.expects.trim() : "";
|
|
212
|
+
if (expects)
|
|
213
|
+
node_.expects = expects.slice(0, 80);
|
|
214
|
+
}
|
|
201
215
|
// Only the two arrangers descend. A leaf with children is a leaf that was
|
|
202
216
|
// labelled wrongly, and its children would render nowhere.
|
|
203
217
|
if (CONTAINERS.has(as) && Array.isArray(node.children)) {
|
package/dist/commands/import.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
3
|
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
4
4
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
@@ -6,7 +6,7 @@ import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
|
6
6
|
import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
|
|
7
7
|
import { isNearDuplicate, isNeutral, lightness, } from "../doctor/color-distance.js";
|
|
8
8
|
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
9
|
-
import { crosswalk, observedRules } from "../doctor/crosswalk.js";
|
|
9
|
+
import { crosswalk, isLibrary, observedRules } from "../doctor/crosswalk.js";
|
|
10
10
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
11
11
|
import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
|
|
12
12
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
@@ -207,7 +207,8 @@ function distinctValues(d) {
|
|
|
207
207
|
}
|
|
208
208
|
return kept.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
209
209
|
}
|
|
210
|
-
export async function takeCensus(root) {
|
|
210
|
+
export async function takeCensus(root, opts) {
|
|
211
|
+
const scopeLabel = opts?.scopeLabel;
|
|
211
212
|
const css = await harvestOwnCss(root);
|
|
212
213
|
const table = buildTable({ css, source: "yours" });
|
|
213
214
|
const schemes = parseSchemeBlocks(css);
|
|
@@ -260,6 +261,48 @@ export async function takeCensus(root) {
|
|
|
260
261
|
}
|
|
261
262
|
const d = diagnose(reports);
|
|
262
263
|
const inventory = tallyToInventory(tally);
|
|
264
|
+
/**
|
|
265
|
+
* THE EVIDENCE PASS - a second walk that touches ONE reading.
|
|
266
|
+
*
|
|
267
|
+
* Only the tally. Not `css`, not `declaredValues`, not `looks`, not `defined`,
|
|
268
|
+
* not `reports`: an app's classes are choices made from a scale, and folding
|
|
269
|
+
* them into the census would put the average back where the declaration goes.
|
|
270
|
+
*
|
|
271
|
+
* Deliberately its own tally so the LIBRARY QUESTION is asked of the scope
|
|
272
|
+
* alone. Fold the app in first and every export has a count, `isLibrary` reads
|
|
273
|
+
* false, and the crosswalk starts substituting the components it was just
|
|
274
|
+
* taught to keep.
|
|
275
|
+
*/
|
|
276
|
+
const usageTally = emptyTally();
|
|
277
|
+
const usageSeen = new Set();
|
|
278
|
+
/**
|
|
279
|
+
* THE SCOPE'S OWN PACKAGE NAME IS INTERNAL, always.
|
|
280
|
+
*
|
|
281
|
+
* An app importing the system says `import { Button } from "@acme/ui"`, and the
|
|
282
|
+
* alias reader finds that name by walking the workspace for sibling manifests. It
|
|
283
|
+
* usually does - but when it cannot, every component the app composes files as
|
|
284
|
+
* THIRD-PARTY and the evidence pass counts nothing. The scope is the system by
|
|
285
|
+
* definition, so its name never needs discovering.
|
|
286
|
+
*/
|
|
287
|
+
const scopePkg = await readFile(join(root, "package.json"), "utf8")
|
|
288
|
+
.then((raw) => JSON.parse(raw).name ?? null)
|
|
289
|
+
.catch(() => null);
|
|
290
|
+
const shared = scopePkg ? [...internal, scopePkg] : internal;
|
|
291
|
+
for (const u of opts?.usage ?? []) {
|
|
292
|
+
const uInternal = await internalSpecifiers(u.path);
|
|
293
|
+
for await (const file of walk(u.path)) {
|
|
294
|
+
const src = await readFile(file, "utf8").catch(() => "");
|
|
295
|
+
if (!src)
|
|
296
|
+
continue;
|
|
297
|
+
const rel = join(u.label, relative(u.path, file));
|
|
298
|
+
if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
usageSeen.add(rel);
|
|
302
|
+
scanComponentsInto(usageTally, rel, src, [...shared, ...uInternal]);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const usedThere = tallyToInventory(usageTally, 400);
|
|
263
306
|
/**
|
|
264
307
|
* THE TWO READINGS BECOME ONE LIST before anything is judged.
|
|
265
308
|
*
|
|
@@ -285,6 +328,9 @@ export async function takeCensus(root) {
|
|
|
285
328
|
...Object.fromEntries(d.flags.map((f) => [f, ["true"]])),
|
|
286
329
|
},
|
|
287
330
|
propFiles: {},
|
|
331
|
+
// Their own rung, off the folder. It travels from the DEFINITION because
|
|
332
|
+
// that is the only reading that knows which file the component lives in.
|
|
333
|
+
...(d.tier ? { tier: d.tier } : {}),
|
|
288
334
|
}));
|
|
289
335
|
/**
|
|
290
336
|
* A composed component that is ALSO defined keeps its usage and gains the
|
|
@@ -306,20 +352,104 @@ export async function takeCensus(root) {
|
|
|
306
352
|
const declaredBy = new Map(defined.map((d) => [d.name, d]));
|
|
307
353
|
const enriched = inventory.map((c) => {
|
|
308
354
|
const d = declaredBy.get(c.name);
|
|
309
|
-
if (!d
|
|
355
|
+
if (!d)
|
|
310
356
|
return c;
|
|
311
|
-
|
|
357
|
+
const tier = d.tier ? { tier: d.tier } : {};
|
|
358
|
+
if (Object.keys(d.axes).length === 0) {
|
|
359
|
+
return Object.keys(tier).length > 0 ? { ...c, ...tier } : c;
|
|
360
|
+
}
|
|
361
|
+
return { ...c, declaredAxes: d.axes, ...tier };
|
|
362
|
+
});
|
|
363
|
+
const scoped = [...enriched, ...fromTypes];
|
|
364
|
+
/**
|
|
365
|
+
* THE EVIDENCE, FOLDED IN - counts and chosen values, never new components.
|
|
366
|
+
*
|
|
367
|
+
* A component the app has and the system does not is the app's own, and adopting
|
|
368
|
+
* it here would make the system a union of two codebases. It is reported instead:
|
|
369
|
+
* a number somebody can act on beats a component nobody asked for.
|
|
370
|
+
*
|
|
371
|
+
* The counts ADD because they are the same act measured in two places, and the
|
|
372
|
+
* prop values union because both are things somebody typed. `propFiles` is what
|
|
373
|
+
* the laws read, and it is the reason this pass exists: three files agreeing is
|
|
374
|
+
* what makes a habit, and a library has one file per component.
|
|
375
|
+
*/
|
|
376
|
+
const own = new Set(scoped.map((c) => c.name));
|
|
377
|
+
const evidence = new Map(usedThere.filter((c) => !c.from).map((c) => [c.name, c]));
|
|
378
|
+
const merged = scoped.map((c) => {
|
|
379
|
+
const e = evidence.get(c.name);
|
|
380
|
+
if (!e)
|
|
381
|
+
return c;
|
|
382
|
+
/**
|
|
383
|
+
* A COMPONENT COMPOSED NOWHERE IN THE SCOPE carries its DECLARATION in `props`,
|
|
384
|
+
* because for a library that is the only reading there is. The moment real usage
|
|
385
|
+
* arrives, that declaration has to move where declarations live - otherwise the
|
|
386
|
+
* two readings merge and `props` stops meaning "what the code passed".
|
|
387
|
+
*
|
|
388
|
+
* It is the same mistake `declaredAxes` exists to prevent, arriving from the
|
|
389
|
+
* other side: unioned, a Button declaring solid|ghost and only ever passed solid
|
|
390
|
+
* reports two options, and the law that all four files agree can never be
|
|
391
|
+
* written.
|
|
392
|
+
*/
|
|
393
|
+
const declaredOnly = (c.count ?? 0) === 0;
|
|
394
|
+
const props = declaredOnly ? {} : { ...c.props };
|
|
395
|
+
for (const [prop, values] of Object.entries(e.props)) {
|
|
396
|
+
props[prop] = [...new Set([...(props[prop] ?? []), ...values])];
|
|
397
|
+
}
|
|
398
|
+
const declaredAxes = c.declaredAxes ?? (declaredOnly ? c.props : undefined);
|
|
399
|
+
const propFiles = { ...(c.propFiles ?? {}) };
|
|
400
|
+
for (const [prop, n] of Object.entries(e.propFiles ?? {})) {
|
|
401
|
+
propFiles[prop] = (propFiles[prop] ?? 0) + n;
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
...c,
|
|
405
|
+
count: (c.count ?? 0) + (e.count ?? 0),
|
|
406
|
+
files: (c.files ?? 0) + (e.files ?? 0),
|
|
407
|
+
props,
|
|
408
|
+
propFiles,
|
|
409
|
+
...(declaredAxes && Object.keys(declaredAxes).length > 0
|
|
410
|
+
? { declaredAxes }
|
|
411
|
+
: {}),
|
|
412
|
+
};
|
|
312
413
|
});
|
|
313
|
-
const
|
|
414
|
+
const theirsOnly = usedThere.filter((c) => !c.from && !own.has(c.name));
|
|
314
415
|
// The verdict travels WITH the payload: the platform reads one reading rather
|
|
315
416
|
// than computing a second opinion from the same numbers, which is how two
|
|
316
417
|
// implementations of the same judgement start disagreeing.
|
|
317
|
-
|
|
418
|
+
/**
|
|
419
|
+
* IS THIS SCOPE A LIBRARY? The answer changes what the crosswalk is FOR.
|
|
420
|
+
*
|
|
421
|
+
* In an app, "your Pill reads as our badge" saves them a duplicate. In a library it
|
|
422
|
+
* would delete their component - and 16 of 37 in a real one were deleted that way
|
|
423
|
+
* (dono, 01/08). Measured, not guessed: a library declares and does not compose
|
|
424
|
+
* itself. See `isLibrary`.
|
|
425
|
+
*/
|
|
426
|
+
const library = isLibrary(scoped);
|
|
427
|
+
const verdicts = new Map(crosswalk(merged, { library }).map((r) => [
|
|
318
428
|
r.component.name,
|
|
319
429
|
{ bucket: r.bucket, canonical: r.canonical, because: r.because },
|
|
320
430
|
]));
|
|
431
|
+
if (usedThere.length > 0) {
|
|
432
|
+
console.log("");
|
|
433
|
+
console.log(body(`Read ${usageSeen.size} more file${usageSeen.size === 1 ? "" : "s"} for EVIDENCE only - ${(opts?.usage ?? []).map((u) => paint.strong(u.label)).join(", ")}. What comes from there is how often each component is used and which values get picked, which is what turns a settled choice into a law. The palette, the scales and the convention stay from ${paint.strong(scopeLabel ?? "the system")}: an app's average is not a scale, it is a set of choices made from one.`));
|
|
434
|
+
if (theirsOnly.length > 0) {
|
|
435
|
+
const shown = theirsOnly.slice(0, 6).map((c) => c.name);
|
|
436
|
+
console.log(body(paint.faint(`(${theirsOnly.length} component${theirsOnly.length === 1 ? "" : "s"} live there and not in the system - ${shown.join(", ")}${theirsOnly.length > shown.length ? ", …" : ""}. Left out on purpose: the system is one place, and a union of two codebases is not a system. Point --scope at them if they belong in it.)`)));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (library) {
|
|
440
|
+
console.log("");
|
|
441
|
+
console.log(body(`This scope DECLARES its components and composes almost none of them inside itself - which is what a component library looks like from the inside. So every component it exports arrives as YOURS, with its own recipe. Where one reads like something in our catalogue, that is recorded as a note instead of replacing it.`));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* THE LAWS READ THE MERGED LIST, not the scope's.
|
|
445
|
+
*
|
|
446
|
+
* `RULE_MIN_FILES` is three, and a library has one file per component - so
|
|
447
|
+
* measured against the scope alone, "every file that passes it agrees" can never
|
|
448
|
+
* be true and the habit rung stays empty forever. The app is where the agreeing
|
|
449
|
+
* happens, which is the whole reason `--usage` exists.
|
|
450
|
+
*/
|
|
321
451
|
const laws = new Map();
|
|
322
|
-
for (const r of observedRules(
|
|
452
|
+
for (const r of observedRules(merged)) {
|
|
323
453
|
laws.set(r.component, [...(laws.get(r.component) ?? []), r.text]);
|
|
324
454
|
}
|
|
325
455
|
const components = merged.map((c) => ({
|
|
@@ -361,6 +491,9 @@ export async function takeCensus(root) {
|
|
|
361
491
|
observed: distinctValues(d),
|
|
362
492
|
...(components.length > 0 ? { components } : {}),
|
|
363
493
|
...(defined.length > 0 ? { defined } : {}),
|
|
494
|
+
...((opts?.usage ?? []).length > 0
|
|
495
|
+
? { usage: (opts?.usage ?? []).map((u) => u.label) }
|
|
496
|
+
: {}),
|
|
364
497
|
totals: {
|
|
365
498
|
scanned: d.scanned,
|
|
366
499
|
values: d.findings.length,
|
|
@@ -670,9 +803,22 @@ async function sayIfWorkspace(root) {
|
|
|
670
803
|
console.log(body("That is a fine DIAGNOSIS and a poor system: a light app and a dark one"));
|
|
671
804
|
console.log(body("average into a palette that is neither."));
|
|
672
805
|
console.log("");
|
|
806
|
+
/**
|
|
807
|
+
* BOTH ROLES IN ONE LINE, because a person reading this has both folders open.
|
|
808
|
+
*
|
|
809
|
+
* `--scope` alone is where the library came back with no laws at all: the system
|
|
810
|
+
* is one place, but the EVIDENCE that a choice is settled is somewhere else, and
|
|
811
|
+
* naming only the first left the second unreachable (dono, 01/08).
|
|
812
|
+
*/
|
|
813
|
+
const usageFlags = apps
|
|
814
|
+
.slice(0, 2)
|
|
815
|
+
.map((a) => `--usage ${a}`)
|
|
816
|
+
.join(" ");
|
|
673
817
|
if (shared.length > 0) {
|
|
674
818
|
console.log(body("The shape that works, if your vocabulary is shared:"));
|
|
675
|
-
console.log(body(` ${paint.strong(`synthesisui import --scope ${shared[0]}
|
|
819
|
+
console.log(body(` ${paint.strong(`synthesisui import --scope ${shared[0]} ${usageFlags}`)}`));
|
|
820
|
+
console.log(body(paint.faint(` --scope is the SYSTEM: the tokens, the components, the way you name a class. One place.`)));
|
|
821
|
+
console.log(body(paint.faint(` --usage is the EVIDENCE: how often each component is used and which values get picked. As many as you have.`)));
|
|
676
822
|
}
|
|
677
823
|
else {
|
|
678
824
|
console.log(body("The shape that works:"));
|
|
@@ -1017,11 +1163,40 @@ export async function runImport(opts) {
|
|
|
1017
1163
|
* decisions live in one package and the governance belongs at the root, and
|
|
1018
1164
|
* conflating the two meant every re-measure scattered another census.
|
|
1019
1165
|
*/
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1166
|
+
const tidy = (p) => p
|
|
1167
|
+
.trim()
|
|
1022
1168
|
.replace(/^\.\/+/, "")
|
|
1023
1169
|
.replace(/\/+$/, "");
|
|
1170
|
+
const scope = opts.scope ? tidy(opts.scope) : undefined;
|
|
1024
1171
|
const readFrom = scope ? join(root, scope) : root;
|
|
1172
|
+
/**
|
|
1173
|
+
* THE SECOND ROLE. `--scope` is the SYSTEM - one place, and the only source of
|
|
1174
|
+
* the palette, the scales and the convention. `--usage` is the EVIDENCE - as
|
|
1175
|
+
* many places as they have, and the only source of counts, chosen values and
|
|
1176
|
+
* laws.
|
|
1177
|
+
*
|
|
1178
|
+
* They were one flag, and one flag could only answer one of the two questions.
|
|
1179
|
+
* Pointed at a real library, `usage` came back empty on all 23 components and
|
|
1180
|
+
* the habit rung of the ladder was unreachable by construction: a law needs
|
|
1181
|
+
* three files that agree and a library has one file per component (dono, 01/08).
|
|
1182
|
+
*/
|
|
1183
|
+
const usage = [];
|
|
1184
|
+
for (const raw of opts.usage ?? []) {
|
|
1185
|
+
const label = tidy(raw);
|
|
1186
|
+
if (!label || label === scope)
|
|
1187
|
+
continue;
|
|
1188
|
+
const path = join(root, label);
|
|
1189
|
+
const ok = await stat(path)
|
|
1190
|
+
.then((st) => st.isDirectory())
|
|
1191
|
+
.catch(() => false);
|
|
1192
|
+
if (!ok) {
|
|
1193
|
+
console.log(section("Import"));
|
|
1194
|
+
console.log(body(`\`--usage ${label}\` is not a folder under ${paint.strong(root)}. Nothing was read.`));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (!usage.some((u) => u.label === label))
|
|
1198
|
+
usage.push({ path, label });
|
|
1199
|
+
}
|
|
1025
1200
|
// A census handed to us (an agent annotated it) is sent as-is; the numbers
|
|
1026
1201
|
// inside were still ours to begin with.
|
|
1027
1202
|
let census;
|
|
@@ -1060,7 +1235,10 @@ export async function runImport(opts) {
|
|
|
1060
1235
|
}
|
|
1061
1236
|
else {
|
|
1062
1237
|
console.log(section("Reading your project"));
|
|
1063
|
-
census = await takeCensus(readFrom
|
|
1238
|
+
census = await takeCensus(readFrom, {
|
|
1239
|
+
...(usage.length > 0 ? { usage } : {}),
|
|
1240
|
+
...(scope ? { scopeLabel: scope } : {}),
|
|
1241
|
+
});
|
|
1064
1242
|
if (scope)
|
|
1065
1243
|
census.scope = scope;
|
|
1066
1244
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -232,6 +232,15 @@ async function describeComponent(root, name) {
|
|
|
232
232
|
const out = [
|
|
233
233
|
`${name}${recipe.description ? ` - ${recipe.description}` : ""}`,
|
|
234
234
|
];
|
|
235
|
+
// THEIR OWN RUNG, when their repo has one. An agent about to compose this should
|
|
236
|
+
// know whether it is a primitive or a whole region before it starts.
|
|
237
|
+
if (recipe.tier) {
|
|
238
|
+
out.push(recipe.tier === "atom"
|
|
239
|
+
? "An atom in your own ladder - nothing else in the system breaks down into it."
|
|
240
|
+
: recipe.tier === "molecule"
|
|
241
|
+
? "A molecule in your own ladder - built out of your atoms, so reach for those inside it."
|
|
242
|
+
: "An organism in your own ladder - a whole region of a screen, so it is more likely to be composed than to be composed into.");
|
|
243
|
+
}
|
|
235
244
|
// WHAT IT SITS ON. 17 of 23 components in a real library carry no surface of
|
|
236
245
|
// their own, because the surface belongs to what they return.
|
|
237
246
|
const p = recipe.preview;
|
|
@@ -516,6 +516,7 @@ const FORM_TAG = {
|
|
|
516
516
|
icon: { tag: "span" },
|
|
517
517
|
row: { tag: "div" },
|
|
518
518
|
stack: { tag: "div" },
|
|
519
|
+
slot: { tag: "div" },
|
|
519
520
|
};
|
|
520
521
|
/** Only these two arrange; the rest are leaves. */
|
|
521
522
|
const ARRANGES = new Set(["row", "stack"]);
|
|
@@ -555,6 +556,16 @@ function emitTree(nodes, comp, indent) {
|
|
|
555
556
|
lines.push(`${indent}{/* ${node.from} renders here - a third party's component, so its markup is theirs */}`);
|
|
556
557
|
continue;
|
|
557
558
|
}
|
|
559
|
+
// The caller's content, so the generated code takes it as a prop rather than
|
|
560
|
+
// inventing a sample. A slot that also carries styles keeps its part element
|
|
561
|
+
// around the children; a bare one is just `{children}`.
|
|
562
|
+
if (node.as === "slot") {
|
|
563
|
+
const inner = `{children}${node.expects ? ` {/* ${node.expects} */}` : ""}`;
|
|
564
|
+
lines.push(node.part
|
|
565
|
+
? `${indent}<${comp}${pascal(node.part)}>${inner}</${comp}${pascal(node.part)}>`
|
|
566
|
+
: `${indent}${inner}`);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
558
569
|
if (!node.part) {
|
|
559
570
|
// Pure structure with no styles of its own: keep the arrangement, skip the
|
|
560
571
|
// element - a div that carries nothing is a div nobody needs.
|
|
@@ -13,17 +13,30 @@ const LITERAL_PROP = /([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*\{?\s*["']([^"']*)["']\s*\}
|
|
|
13
13
|
/** `dense` with no value is a boolean prop set to true, and that IS a literal
|
|
14
14
|
* decision worth counting. */
|
|
15
15
|
const BARE_PROP = /(^|\s)([a-z][a-zA-Z0-9_]*)(?=\s|$)/g;
|
|
16
|
+
/**
|
|
17
|
+
* A KEYWORD IS NOT AN IDENTIFIER, and the difference decides whether a `<` opens
|
|
18
|
+
* an element. `return <Card>one</Card>` is a component; `useState<Foo>()` is a
|
|
19
|
+
* type argument. Both are preceded by a word.
|
|
20
|
+
*
|
|
21
|
+
* Prettier writes `return <Foo />;` on one line whenever it fits, so without this
|
|
22
|
+
* every short component written that way read as a generic and vanished from the
|
|
23
|
+
* inventory - which is how a spec for something else caught it.
|
|
24
|
+
*/
|
|
25
|
+
const JSX_BEFORE = /\b(return|case|default|await|yield|typeof|in|of|do|else|new)$/;
|
|
16
26
|
/**
|
|
17
27
|
* A generic in type position (`useState<Foo>()`, `Array<Item>`) is not an
|
|
18
28
|
* element. The tell is what precedes the `<`: an identifier or a closing paren
|
|
19
|
-
* means a type argument, while JSX follows `(`, `{`, `>`, a newline or
|
|
29
|
+
* means a type argument, while JSX follows `(`, `{`, `>`, a newline, a keyword or
|
|
30
|
+
* nothing.
|
|
20
31
|
*/
|
|
21
32
|
function looksLikeType(source, at) {
|
|
22
33
|
for (let i = at - 1; i >= 0; i--) {
|
|
23
34
|
const c = source[i];
|
|
24
35
|
if (c === " " || c === "\t")
|
|
25
36
|
continue;
|
|
26
|
-
|
|
37
|
+
if (!/[A-Za-z0-9_)\]]/.test(c))
|
|
38
|
+
return false;
|
|
39
|
+
return !JSX_BEFORE.test(source.slice(Math.max(0, i - 11), i + 1));
|
|
27
40
|
}
|
|
28
41
|
return false;
|
|
29
42
|
}
|
|
@@ -81,6 +94,56 @@ function importedNames(source, internal = []) {
|
|
|
81
94
|
}
|
|
82
95
|
return out;
|
|
83
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Capitalised names a file BINDS ITSELF and does not export.
|
|
99
|
+
*
|
|
100
|
+
* `<Component>` and `<CustomLegend/>` both arrived in a real library's inventory as
|
|
101
|
+
* components of the system (dono, 01/08). Neither is one:
|
|
102
|
+
*
|
|
103
|
+
* function Text({ as: Component = "p" }) // React's polymorphic idiom -
|
|
104
|
+
* // `Component` is a prop rename
|
|
105
|
+
* const CustomLegend = () => (…) // a closure inside LineChart
|
|
106
|
+
*
|
|
107
|
+
* Capitalisation is React's rule for "not an html tag", which is not the same
|
|
108
|
+
* question as "is this a component the system offers". The test that separates them
|
|
109
|
+
* is REACHABILITY: a component of the system is either imported from somewhere or
|
|
110
|
+
* exported to somewhere. A name that only exists inside one function body is that
|
|
111
|
+
* function's private wiring, and giving it a recipe invents a component nobody can
|
|
112
|
+
* import.
|
|
113
|
+
*
|
|
114
|
+
* Local AND exported stays, of course - that is just a component defined where it
|
|
115
|
+
* is used.
|
|
116
|
+
*/
|
|
117
|
+
const BOUND = /(?:const|let|var|function|class)\s+([A-Z][A-Za-z0-9_]*)|[{,]\s*\w+\s*:\s*([A-Z][A-Za-z0-9_]*)\s*(?:=[^,}]*)?\s*[,}]/g;
|
|
118
|
+
const EXPORTED_NAME = /export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+([A-Z][A-Za-z0-9_]*)|export\s+default\s+([A-Z][A-Za-z0-9_]*)|export\s*\{([^}]*)\}/g;
|
|
119
|
+
export function localOnlyNames(source) {
|
|
120
|
+
const exported = new Set();
|
|
121
|
+
EXPORTED_NAME.lastIndex = 0;
|
|
122
|
+
for (const m of source.matchAll(EXPORTED_NAME)) {
|
|
123
|
+
if (m[1])
|
|
124
|
+
exported.add(m[1]);
|
|
125
|
+
if (m[2])
|
|
126
|
+
exported.add(m[2]);
|
|
127
|
+
if (m[3]) {
|
|
128
|
+
for (const raw of m[3].split(",")) {
|
|
129
|
+
const name = raw
|
|
130
|
+
.trim()
|
|
131
|
+
.split(/\s+as\s+/)[0]
|
|
132
|
+
?.trim();
|
|
133
|
+
if (name && /^[A-Z]/.test(name))
|
|
134
|
+
exported.add(name);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const out = new Set();
|
|
139
|
+
BOUND.lastIndex = 0;
|
|
140
|
+
for (const m of source.matchAll(BOUND)) {
|
|
141
|
+
const name = m[1] ?? m[2];
|
|
142
|
+
if (name && !exported.has(name))
|
|
143
|
+
out.add(name);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
84
147
|
export function emptyTally() {
|
|
85
148
|
return new Map();
|
|
86
149
|
}
|
|
@@ -92,11 +155,16 @@ internal = []) {
|
|
|
92
155
|
if (!/\.(tsx|jsx|vue|svelte)$/i.test(file))
|
|
93
156
|
return;
|
|
94
157
|
const owners = importedNames(source, internal);
|
|
158
|
+
const private_ = localOnlyNames(source);
|
|
95
159
|
ELEMENT.lastIndex = 0;
|
|
96
160
|
for (const m of source.matchAll(ELEMENT)) {
|
|
97
161
|
if (m.index != null && looksLikeType(source, m.index))
|
|
98
162
|
continue;
|
|
99
163
|
const name = m[1];
|
|
164
|
+
// Private wiring, not a component of the system. An import wins: a file may
|
|
165
|
+
// shadow nothing and still bind a name it also imports.
|
|
166
|
+
if (!owners.has(name) && private_.has(name.split(".")[0]))
|
|
167
|
+
continue;
|
|
100
168
|
const body = m[2] ?? "";
|
|
101
169
|
let hit = tally.get(name);
|
|
102
170
|
if (!hit) {
|
package/dist/doctor/crosswalk.js
CHANGED
|
@@ -304,8 +304,45 @@ function sharedMeaning(a, b) {
|
|
|
304
304
|
n += 1;
|
|
305
305
|
return n;
|
|
306
306
|
}
|
|
307
|
-
|
|
307
|
+
/**
|
|
308
|
+
* WHEN THE SOURCE IS THE LIBRARY, EVERY COMPONENT IS THEIRS.
|
|
309
|
+
*
|
|
310
|
+
* The crosswalk was designed for an APP that uses components: "your Pill reads as
|
|
311
|
+
* our badge, use ours" is exactly right there, because it stops a duplicate. When the
|
|
312
|
+
* source IS the component library, the premise inverts - their library IS the design
|
|
313
|
+
* system, and mapping their component onto ours is us overwriting theirs with ours.
|
|
314
|
+
* The opposite of entering as a complement.
|
|
315
|
+
*
|
|
316
|
+
* Measured on a real library (dono, 01/08): of 37 components they export, 16 never
|
|
317
|
+
* became theirs. `Label`, `Pill` and `Tag` - three distinct components - collapsed
|
|
318
|
+
* into one `ds-badge` of ours, which is why their Tag previewed as the word "Pill":
|
|
319
|
+
* that is OUR recipe's sample text. `LineChart`, a real organism, simply was not in
|
|
320
|
+
* the system.
|
|
321
|
+
*
|
|
322
|
+
* The code already knew. `describeOrigin` says it in prose - "declared in
|
|
323
|
+
* `packages/ui` and composed nowhere inside it, which is what a component library
|
|
324
|
+
* looks like from the inside" - and then acted as if it were an app.
|
|
325
|
+
*
|
|
326
|
+
* THE SIGNAL, and it is not a guess: a library declares and does not compose itself.
|
|
327
|
+
* When most of what a scope declares is composed nowhere inside it, the scope is a
|
|
328
|
+
* library. The crosswalk still runs and its verdict still travels as INFORMATION -
|
|
329
|
+
* "this reads as our badge" is worth knowing - it just stops replacing anything.
|
|
330
|
+
*/
|
|
331
|
+
export function isLibrary(components) {
|
|
332
|
+
const mine = components.filter((c) => !c.from);
|
|
333
|
+
if (mine.length < 4)
|
|
334
|
+
return false;
|
|
335
|
+
const uncomposed = mine.filter((c) => (c.count ?? 0) === 0).length;
|
|
336
|
+
return uncomposed / mine.length >= 0.5;
|
|
337
|
+
}
|
|
338
|
+
export function crosswalk(components,
|
|
339
|
+
/**
|
|
340
|
+
* Their library is the system. Every component keeps its own recipe, and the
|
|
341
|
+
* catalogue match becomes a note rather than a substitution.
|
|
342
|
+
*/
|
|
343
|
+
opts) {
|
|
308
344
|
const mine = components.filter((c) => !c.from);
|
|
345
|
+
const library = opts?.library ?? false;
|
|
309
346
|
// Two of THEIR components reading as the same catalogue entry is the
|
|
310
347
|
// strongest redundancy signal there is, and it needs no threshold: it is not
|
|
311
348
|
// "these look alike", it is "you wrote this twice". The first run filed both
|
|
@@ -344,6 +381,24 @@ export function crosswalk(components) {
|
|
|
344
381
|
.sort((a, b) => b.overlap - a.overlap);
|
|
345
382
|
if (canonical && CATALOGUE[canonical]) {
|
|
346
383
|
const alsoClaiming = (claims.get(canonical) ?? []).filter((n) => n !== component.name);
|
|
384
|
+
/**
|
|
385
|
+
* LIBRARY MODE: the match is a NOTE, not a verdict.
|
|
386
|
+
*
|
|
387
|
+
* Their component stays exclusive and keeps its own recipe; `canonical` still
|
|
388
|
+
* travels so a preview can resolve a frontier and so the v2 can say "these
|
|
389
|
+
* three read alike". What stops happening is their `Label` ceasing to exist.
|
|
390
|
+
*/
|
|
391
|
+
if (library) {
|
|
392
|
+
return {
|
|
393
|
+
component,
|
|
394
|
+
canonical,
|
|
395
|
+
twins,
|
|
396
|
+
bucket: "exclusive",
|
|
397
|
+
because: alsoClaiming.length > 0
|
|
398
|
+
? `yours, and it reads like ds-${canonical} - so do ${alsoClaiming.join(", ")}, which is one decision written ${alsoClaiming.length + 1} times`
|
|
399
|
+
: `yours, and it reads like ds-${canonical} - kept as yours because this library IS the system`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
347
402
|
if (alsoClaiming.length > 0) {
|
|
348
403
|
return {
|
|
349
404
|
component,
|
|
@@ -369,7 +424,9 @@ export function crosswalk(components) {
|
|
|
369
424
|
component,
|
|
370
425
|
canonical,
|
|
371
426
|
twins,
|
|
372
|
-
|
|
427
|
+
// A twin is a v2 observation either way; in library mode it must not cost
|
|
428
|
+
// the component its own recipe.
|
|
429
|
+
bucket: library ? "exclusive" : "nearly",
|
|
373
430
|
because: `shares ${Math.round(twins[0].overlap * 100)}% of its options with ${twins[0].name} - one decision, two components`,
|
|
374
431
|
};
|
|
375
432
|
}
|
|
@@ -20,6 +20,36 @@
|
|
|
20
20
|
* actually write and stays silent on the rest: a props type built by `Omit<>` or
|
|
21
21
|
* spread from another interface yields no axes rather than wrong ones.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* THEIR OWN LADDER, READ OFF THE PATH - never guessed from a name.
|
|
25
|
+
*
|
|
26
|
+
* A real library keeps `atoms/Text/Text.tsx`, `molecules/MetricCard/`,
|
|
27
|
+
* `organisms/DataTable/`: the author saying out loud what composes what, in the
|
|
28
|
+
* vocabulary the whole industry already shares. Twenty-three components arrived in
|
|
29
|
+
* one flat bucket and the information was sitting in the path (dono, 01/08).
|
|
30
|
+
*
|
|
31
|
+
* A path segment, not a substring: `src/atomsphere/` is a folder called
|
|
32
|
+
* atomsphere. Returns undefined for the projects that do not organise themselves
|
|
33
|
+
* this way, which is most of them - and undefined means "no ladder here", not
|
|
34
|
+
* "unclassified".
|
|
35
|
+
*/
|
|
36
|
+
const TIER_FOLDER = {
|
|
37
|
+
atoms: "atom",
|
|
38
|
+
atom: "atom",
|
|
39
|
+
primitives: "atom",
|
|
40
|
+
molecules: "molecule",
|
|
41
|
+
molecule: "molecule",
|
|
42
|
+
organisms: "organism",
|
|
43
|
+
organism: "organism",
|
|
44
|
+
};
|
|
45
|
+
export function tierOf(file) {
|
|
46
|
+
for (const segment of file.split(/[/\\]/)) {
|
|
47
|
+
const tier = TIER_FOLDER[segment.toLowerCase()];
|
|
48
|
+
if (tier)
|
|
49
|
+
return tier;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
23
53
|
/** `export function X`, `export default function X`, `export const X =`. */
|
|
24
54
|
const EXPORTED = /export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Z][A-Za-z0-9_]*)|const\s+([A-Z][A-Za-z0-9_]*)\s*[:=])/g;
|
|
25
55
|
/** `type ButtonProps = { … }` or `interface ButtonProps { … }`, to its closing
|
|
@@ -78,11 +108,13 @@ export function scanDefinitions(file, source) {
|
|
|
78
108
|
continue;
|
|
79
109
|
seen.add(name);
|
|
80
110
|
const props = propsOf.get(name);
|
|
111
|
+
const tier = tierOf(file);
|
|
81
112
|
out.push({
|
|
82
113
|
name,
|
|
83
114
|
file,
|
|
84
115
|
axes: props?.axes ?? {},
|
|
85
116
|
flags: props?.flags ?? [],
|
|
117
|
+
...(tier ? { tier } : {}),
|
|
86
118
|
});
|
|
87
119
|
}
|
|
88
120
|
return out;
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ Usage - deterministic, FREE:
|
|
|
41
41
|
|
|
42
42
|
Usage - governance (deterministic, FREE):
|
|
43
43
|
synthesisui adopt [--write] turn the design system you ALREADY have into a contract
|
|
44
|
-
synthesisui import [--
|
|
44
|
+
synthesisui import [--scope <p>] [--usage <p>] read what you already have and make it a system
|
|
45
45
|
your agent follows - without touching your CSS
|
|
46
46
|
synthesisui connect wire your agent: the check as an editor hook, the system
|
|
47
47
|
as MCP tools, and a contract that stops repeating itself
|
|
@@ -66,7 +66,8 @@ Options:
|
|
|
66
66
|
--version <n> install a specific version (default: latest)
|
|
67
67
|
--ds <slug> init: bring this DS in right away · generate: target DS (default: installed)
|
|
68
68
|
--name <name> preferred component name for generate
|
|
69
|
-
--scope <path> import:
|
|
69
|
+
--scope <path> import: the SYSTEM - tokens, components, convention. One folder.
|
|
70
|
+
--usage <path> import: the EVIDENCE - counts, chosen values, laws. Repeatable.
|
|
70
71
|
--scheme <s> dark|light - which end of your ladder is the default
|
|
71
72
|
--target <t> template/init target: next | general (default: next)
|
|
72
73
|
--pages-dir <dir> init: folder for generated pages (default: app)
|
|
@@ -111,9 +112,30 @@ Examples:
|
|
|
111
112
|
synthesisui generate "an upgrade banner with a title, message and a primary CTA"
|
|
112
113
|
`;
|
|
113
114
|
/** Extracts simple `--flag value` pairs and the remaining positionals. */
|
|
115
|
+
/**
|
|
116
|
+
* A REPEATED FLAG ACCUMULATES rather than overwriting.
|
|
117
|
+
*
|
|
118
|
+
* `--usage apps/web --usage apps/admin` is the natural way to say two, and keeping
|
|
119
|
+
* only the last would drop one silently - which is the failure mode this codebase
|
|
120
|
+
* refuses everywhere else. Every existing flag is passed once and still arrives as
|
|
121
|
+
* a string, so nothing that reads `typeof flags.x === "string"` changes.
|
|
122
|
+
*/
|
|
114
123
|
function parseFlags(argv) {
|
|
115
124
|
const positionals = [];
|
|
116
125
|
const flags = {};
|
|
126
|
+
const put = (key, value) => {
|
|
127
|
+
const had = flags[key];
|
|
128
|
+
if (had === undefined) {
|
|
129
|
+
flags[key] = value;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (typeof value !== "string")
|
|
133
|
+
return;
|
|
134
|
+
flags[key] = [
|
|
135
|
+
...(Array.isArray(had) ? had : typeof had === "string" ? [had] : []),
|
|
136
|
+
value,
|
|
137
|
+
];
|
|
138
|
+
};
|
|
117
139
|
for (let i = 0; i < argv.length; i++) {
|
|
118
140
|
const arg = argv[i];
|
|
119
141
|
if (arg === "-h" || arg === "--help") {
|
|
@@ -124,17 +146,17 @@ function parseFlags(argv) {
|
|
|
124
146
|
const eq = body.indexOf("=");
|
|
125
147
|
if (eq !== -1) {
|
|
126
148
|
// `--key=value` form (e.g. --out=templates/page.tsx)
|
|
127
|
-
|
|
149
|
+
put(body.slice(0, eq), body.slice(eq + 1));
|
|
128
150
|
}
|
|
129
151
|
else {
|
|
130
152
|
// `--key value` form
|
|
131
153
|
const next = argv[i + 1];
|
|
132
154
|
if (next !== undefined && !next.startsWith("--")) {
|
|
133
|
-
|
|
155
|
+
put(body, next);
|
|
134
156
|
i++;
|
|
135
157
|
}
|
|
136
158
|
else {
|
|
137
|
-
|
|
159
|
+
put(body, true);
|
|
138
160
|
}
|
|
139
161
|
}
|
|
140
162
|
}
|
|
@@ -165,6 +187,14 @@ async function main() {
|
|
|
165
187
|
// invert a system, so it falls through to being measured and asked.
|
|
166
188
|
// WHAT to read. `--dir` stays WHERE the project is, in every command.
|
|
167
189
|
scope: typeof flags.scope === "string" ? flags.scope : undefined,
|
|
190
|
+
// WHERE THE EVIDENCE IS. Repeatable, and comma-separated works too - a
|
|
191
|
+
// person typing one path with a comma in it means two paths.
|
|
192
|
+
usage: [flags.usage]
|
|
193
|
+
.flat()
|
|
194
|
+
.filter((v) => typeof v === "string")
|
|
195
|
+
.flatMap((v) => v.split(","))
|
|
196
|
+
.map((v) => v.trim())
|
|
197
|
+
.filter(Boolean),
|
|
168
198
|
scheme: flags.scheme === "dark" || flags.scheme === "light"
|
|
169
199
|
? flags.scheme
|
|
170
200
|
: undefined,
|
package/dist/skill-import.js
CHANGED
|
@@ -105,22 +105,42 @@ This root holds several projects
|
|
|
105
105
|
is the average of all of them. That is a fine DIAGNOSIS and a poor system: a
|
|
106
106
|
light app and a dark one average into a palette that is neither.
|
|
107
107
|
|
|
108
|
-
synthesisui import --scope packages/ui
|
|
108
|
+
synthesisui import --scope packages/ui --usage apps/web-dashboard
|
|
109
109
|
\`\`\`
|
|
110
110
|
|
|
111
|
-
Then measure again, narrowed
|
|
111
|
+
Then measure again, narrowed - and **name both roles**:
|
|
112
112
|
|
|
113
113
|
\`\`\`
|
|
114
|
-
npx synthesisui import --dry --scope packages/ui
|
|
114
|
+
npx synthesisui import --dry --scope packages/ui --usage apps/web-dashboard
|
|
115
115
|
\`\`\`
|
|
116
116
|
|
|
117
|
+
### The two roles, and why one flag could not do both
|
|
118
|
+
|
|
119
|
+
\`\`\`
|
|
120
|
+
--scope the SYSTEM tokens, components, the way they name a class ONE folder
|
|
121
|
+
--usage the EVIDENCE how often, which values get picked, the laws as many as they have
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
\`--usage\` is repeatable and never contributes a token, a scale or a component. That split is
|
|
125
|
+
not tidiness, it is the two mistakes this pipeline exists to avoid, and each one has been made:
|
|
126
|
+
|
|
117
127
|
**Do not import the average.** Three apps measured together produce a palette that belongs to
|
|
118
128
|
none of them, and it will look plausible - a real monorepo reported 36% coverage across 2797
|
|
119
129
|
files at the root and 47% across 114 in its shared package. The second number is the system;
|
|
120
|
-
the first is a diagnosis.
|
|
130
|
+
the first is a diagnosis. Their scale is a DECLARATION; an app's average is a set of choices
|
|
131
|
+
made from one.
|
|
132
|
+
|
|
133
|
+
**Do not import a library alone either.** A law needs three files that agree, and a library has
|
|
134
|
+
ONE file per component - so measured by itself, a real one came back with \`usage\` empty on all
|
|
135
|
+
23 components and not a single observed law. Not because nothing is settled: because the
|
|
136
|
+
agreeing happens in the app (dono, 01/08). If they have apps, name them.
|
|
121
137
|
|
|
122
|
-
\`--
|
|
123
|
-
|
|
138
|
+
Point \`--usage\` at every app that consumes the system. A component that lives in an app and
|
|
139
|
+
not in the system is reported and left out - the system is one place, and a union of two
|
|
140
|
+
codebases is not a system.
|
|
141
|
+
|
|
142
|
+
The census still lands at \`<root>/_synthesisui/census.json\` and records both what it measured
|
|
143
|
+
and where the evidence came from, so you always send the same path and there is never a second
|
|
124
144
|
file to pick between. Nothing is sent by \`--dry\`. Read the file.
|
|
125
145
|
|
|
126
146
|
### 2. Read what arithmetic cannot
|
|
@@ -398,7 +418,7 @@ shell and its own parts inside.
|
|
|
398
418
|
\`"root": "BaseDialog.Root"\`, \`"root": "Popover.Root"\`
|
|
399
419
|
- **a plain tag** - leave it out. \`<div>\`, \`<td>\`, \`<button>\` are not frontiers.
|
|
400
420
|
|
|
401
|
-
### The
|
|
421
|
+
### The ten forms
|
|
402
422
|
|
|
403
423
|
\`as\` says what a node IS, and the renderer draws that. Nothing else is accepted:
|
|
404
424
|
|
|
@@ -413,10 +433,40 @@ row arranges its children ACROSS
|
|
|
413
433
|
stack arranges its children DOWN
|
|
414
434
|
component their component → needs "ref": the name as their code spells it
|
|
415
435
|
external a library → needs "from": the package name
|
|
436
|
+
slot the CALLER's content → takes "expects": what goes there, in their words
|
|
416
437
|
\`\`\`
|
|
417
438
|
|
|
418
439
|
Only \`row\` and \`stack\` take \`children\`. Everything else is a leaf.
|
|
419
440
|
|
|
441
|
+
### \`{children}\` is a node, not an absence
|
|
442
|
+
|
|
443
|
+
Four things decide what a component looks like on screen, and only the first is in its own source:
|
|
444
|
+
|
|
445
|
+
\`\`\`
|
|
446
|
+
its own classes you are reading them
|
|
447
|
+
what the caller passes <tr>{children}</tr> → slot
|
|
448
|
+
its runtime state value={62} drives the width → a part with no static size
|
|
449
|
+
a library drawing it <EditorContent> → external
|
|
450
|
+
\`\`\`
|
|
451
|
+
|
|
452
|
+
A component whose content is not its own had nothing to send, so it sent nothing and previewed as
|
|
453
|
+
a blank box. A real \`DataTableRow\` is sixteen lines - a \`<tr>\` carrying a border, a hover and
|
|
454
|
+
\`{children}\` - and it came back empty, which is not wrong so much as unreadable (dono, 01/08).
|
|
455
|
+
|
|
456
|
+
So send the slot. \`expects\` is what the caller is supposed to put there, in the words their own
|
|
457
|
+
code uses - read the prop type, the JSDoc, or a call site:
|
|
458
|
+
|
|
459
|
+
\`\`\`json
|
|
460
|
+
{ "as": "slot", "name": "row", "classes": "border-b hover:bg-ocean-50/30", "expects": "the cells" }
|
|
461
|
+
\`\`\`
|
|
462
|
+
|
|
463
|
+
It keeps its \`name\` and \`classes\` like any other node - the border and the hover are real and
|
|
464
|
+
belong on that element. What it does not do is invent content: the preview says "the cells go
|
|
465
|
+
here", which is exactly what the file says.
|
|
466
|
+
|
|
467
|
+
Good \`expects\`: \`"the cells"\`, \`"one row per item"\`, \`"the form"\`, \`"the page body"\`. Leave it
|
|
468
|
+
out rather than writing \`"children"\` - the field is for what a person would call it.
|
|
469
|
+
|
|
420
470
|
**\`name\` is the part name**, and it is what carries the styles - flat, lowercase, kebab. Never
|
|
421
471
|
nested: \`"actions.generate"\` compiles to two CSS classes and is invalid, so name it
|
|
422
472
|
\`"generate-action"\`. Name what it IS - \`label\`, \`value\`, \`delta\`, \`cover\`, \`toolbar\` - because the
|
|
@@ -440,10 +490,22 @@ of. It previews as a block carrying its name, and it becomes a **rule** - \`Arti
|
|
|
440
490
|
That edge is a **fact, not a habit**: you saw it in the definition, so it is true once and for
|
|
441
491
|
all, and it arrives active without waiting for a third sighting.
|
|
442
492
|
|
|
443
|
-
|
|
493
|
+
Three things that are NOT edges, and sending them as such would be wrong:
|
|
444
494
|
|
|
445
495
|
- \`motion.div\`, \`Dialog.Root\`, \`Radio.Item\` - a library's namespace, not a component of theirs
|
|
446
496
|
- an HTML element with a capital in a variable name
|
|
497
|
+
- **a name the file binds itself and does not export.** Capitalisation is React's rule for "not an
|
|
498
|
+
html tag", which is a different question from "is this a component of the system". Both of these
|
|
499
|
+
arrived as components of a real library and neither is one (dono, 01/08):
|
|
500
|
+
|
|
501
|
+
\`\`\`tsx
|
|
502
|
+
function Text({ as: Component = "p" }) // a prop rename - the polymorphic idiom
|
|
503
|
+
const CustomLegend = () => (…) // a closure inside LineChart
|
|
504
|
+
\`\`\`
|
|
505
|
+
|
|
506
|
+
The test is reachability: a component of the system is imported from somewhere or exported to
|
|
507
|
+
somewhere. A name that only lives inside one function body is that function's private wiring,
|
|
508
|
+
and giving it a recipe invents a component nobody can import.
|
|
447
509
|
|
|
448
510
|
If the component came from a package, it is \`external\`, not \`component\`.
|
|
449
511
|
|
|
@@ -497,7 +559,7 @@ spec view has no nesting to draw.
|
|
|
497
559
|
|
|
498
560
|
### 3. Walk them through the decisions, one at a time
|
|
499
561
|
|
|
500
|
-
|
|
562
|
+
Five decisions are theirs. **Ask them as separate questions with selectable options** - use your
|
|
501
563
|
question tool, one call per decision, so they pick instead of reading a wall and composing a
|
|
502
564
|
reply. A single block containing everything is a report, and a report gets read, not answered.
|
|
503
565
|
|
|
@@ -581,6 +643,36 @@ canvas #222326 darkgray-500 - what DashboardLayout paints on <main>
|
|
|
581
643
|
Say it and let them object. Offering five near-blacks to pick between is asking someone to
|
|
582
644
|
re-decide something their code already decided.
|
|
583
645
|
|
|
646
|
+
**3e-bis. Our kit, or theirs alone** - ask this ONLY when they already have a component library,
|
|
647
|
+
which is when \`--scope\` pointed at one and the CLI said so. An app with no components of its own
|
|
648
|
+
has nothing to weigh and should never see the question.
|
|
649
|
+
|
|
650
|
+
\`\`\`
|
|
651
|
+
Every system born here comes with 45 components of ours - button, card, input,
|
|
652
|
+
table, the rest of a working toolkit. You already have 37.
|
|
653
|
+
|
|
654
|
+
add ours to yours (recommended) 68 components, and the 23 governed by your
|
|
655
|
+
own rules are the 23 you wrote
|
|
656
|
+
mine alone 37 components, all of them yours. Our
|
|
657
|
+
layouts that need a component you do not
|
|
658
|
+
have stay out too. Add any of ours later
|
|
659
|
+
from the library, one at a time
|
|
660
|
+
\`\`\`
|
|
661
|
+
|
|
662
|
+
Send it as \`"standards": "none"\` in the reading when they choose the second, and leave the field
|
|
663
|
+
out when they choose the first. **Recommend adding ours**, and say why in one line: a component of
|
|
664
|
+
ours costs nothing until something composes it, and having a \`table\` the day somebody needs a
|
|
665
|
+
table is worth more than a shorter list.
|
|
666
|
+
|
|
667
|
+
Two things to be straight about, because both are true and neither is obvious:
|
|
668
|
+
|
|
669
|
+
- **the foundations come from the seed either way.** Their palette, their scales, their type - all
|
|
670
|
+
theirs - but the seven type slots, the easing and the neutral floor arrive from ours, because
|
|
671
|
+
the alternative to a type scale is no components at all rather than their components.
|
|
672
|
+
- **our layouts are written in our recipes.** A landing that composes \`button\`, \`card\` and
|
|
673
|
+
\`badge\` needs those to exist. Choosing *mine alone* keeps the layouts whose recipes they
|
|
674
|
+
happen to have and drops the rest - a layout pointing at a recipe nobody has renders as holes.
|
|
675
|
+
|
|
584
676
|
**3f. Fonts** - do not ask at all. Report and move on:
|
|
585
677
|
|
|
586
678
|
\`\`\`
|
|
@@ -595,7 +687,7 @@ way. If it declares one, there is nothing to decide.
|
|
|
595
687
|
carries the weight - \`ocean-500\` tells someone more than a square would. When they run
|
|
596
688
|
\`npx synthesisui import\` themselves in a terminal, the CLI paints them.
|
|
597
689
|
|
|
598
|
-
Then, and only after all
|
|
690
|
+
Then, and only after all of them are answered:
|
|
599
691
|
|
|
600
692
|
\`\`\`
|
|
601
693
|
npx synthesisui import --census _synthesisui/census.json --name "<the name>" [--registry <from step 0>]
|
|
@@ -603,7 +695,7 @@ npx synthesisui import --census _synthesisui/census.json --name "<the name>" [--
|
|
|
603
695
|
|
|
604
696
|
**This is the one real write.** Everything before it is on their disk and costs nothing to redo.
|
|
605
697
|
|
|
606
|
-
Pass every answer explicitly - \`--name\`, \`--scope\` if they changed
|
|
698
|
+
Pass every answer explicitly - \`--name\`, \`--scope\` and \`--usage\` if they changed either, and the reading rewritten
|
|
607
699
|
with their primary and their theme. The CLI cannot ask anything when you are the one running it:
|
|
608
700
|
it has no terminal, so its own prompts are skipped by design. **Whatever you did not ask, nobody
|
|
609
701
|
asked.** Skip 3a and the first time they learn where their system came from is when they open
|
|
@@ -627,6 +719,10 @@ proposal. Say all of this in your own words - do not just print the link:
|
|
|
627
719
|
which half was deliberate: nothing was invented, so a component whose classes named no token
|
|
628
720
|
arrives with structure and no colour. The platform shows that as *Not written yet*, not as a
|
|
629
721
|
zero.
|
|
722
|
+
- **their own ladder**, if their repo has one. Components under \`atoms/\`, \`molecules/\` and
|
|
723
|
+
\`organisms/\` arrive grouped that way in the vitrine, read off the folder rather than guessed
|
|
724
|
+
from a name. You do not send this and you should not try to: a project that does not organise
|
|
725
|
+
itself this way gets one honest group instead of three invented ones.
|
|
630
726
|
- **the shape of each component**, if you sent one: its parts nested as they nest in their code,
|
|
631
727
|
the components of theirs it composes, and the libraries it needs. Each edge is also a rule now,
|
|
632
728
|
which is what makes it survive being looked at once.
|
package/package.json
CHANGED