synthesisui 0.16.86 → 0.16.88
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 +9 -0
- package/dist/commands/import.js +26 -0
- package/dist/doctor/css-modules.js +5 -0
- package/dist/doctor/transcribe.js +159 -2
- package/dist/skill-import.js +38 -0
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -194,6 +194,15 @@ root) {
|
|
|
194
194
|
: [];
|
|
195
195
|
const t = transcribe(classes, declared);
|
|
196
196
|
parts[key] = { base: t.base, dark: t.dark, states: t.states };
|
|
197
|
+
/**
|
|
198
|
+
* A drop on a PART is still a drop. The root path already says these out loud;
|
|
199
|
+
* this one silently swallowed `p-spacing-sm` on a real menu popup, so the part
|
|
200
|
+
* arrived with no padding and the not-expressed file had to discover it from
|
|
201
|
+
* the outside (dono, 01/08).
|
|
202
|
+
*/
|
|
203
|
+
for (const cls of t.unreadable) {
|
|
204
|
+
notes.push(`part \`${key}\`: \`${cls}\` starts like a design decision and reads as nothing - usually a typo in the source, and the element renders without it today`);
|
|
205
|
+
}
|
|
197
206
|
node_.part = key;
|
|
198
207
|
}
|
|
199
208
|
const text = typeof node.text === "string" ? node.text.trim() : "";
|
package/dist/commands/import.js
CHANGED
|
@@ -799,6 +799,31 @@ export async function takeCensus(root, opts) {
|
|
|
799
799
|
console.log(body(line));
|
|
800
800
|
console.log(body(paint.faint("Do not go looking for any of this - it is measured, and a sweep would bring back a different slice than the next one.")));
|
|
801
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* THE DROPS, SAID OUT LOUD. `p-spacing-sm` on a real menu popup read as nothing and
|
|
804
|
+
* the popup shipped with no padding, invisibly - "if the compiler drops them, the drop
|
|
805
|
+
* is invisible from here" was the not-expressed file calling this exact hole (dono,
|
|
806
|
+
* 01/08). More often than not the class is malformed in THEIR source, which makes this
|
|
807
|
+
* a defect report about their code, not an apology about ours.
|
|
808
|
+
*/
|
|
809
|
+
const unreadableByComponent = Object.entries(looks)
|
|
810
|
+
.filter(([, look]) => (look.unreadable ?? []).length > 0)
|
|
811
|
+
.map(([component, look]) => ({
|
|
812
|
+
component,
|
|
813
|
+
classes: look.unreadable ?? [],
|
|
814
|
+
}));
|
|
815
|
+
if (unreadableByComponent.length > 0) {
|
|
816
|
+
const total = unreadableByComponent.reduce((n, u) => n + u.classes.length, 0);
|
|
817
|
+
console.log("");
|
|
818
|
+
console.log(section("Design classes that read as nothing"));
|
|
819
|
+
console.log(body(`${total} class${total === 1 ? "" : "es"} start like a design decision and resolve to nothing - usually a typo in the source, and the element renders without that decision today. Nothing was guessed on their behalf:`));
|
|
820
|
+
for (const u of unreadableByComponent.slice(0, 8)) {
|
|
821
|
+
console.log(body(paint.faint(` ${u.component}: ${u.classes.slice(0, 4).join(", ")}`)));
|
|
822
|
+
}
|
|
823
|
+
if (unreadableByComponent.length > 8) {
|
|
824
|
+
console.log(body(paint.faint(` and ${unreadableByComponent.length - 8} more components`)));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
802
827
|
const coverageLines = describeCoverage(coverage);
|
|
803
828
|
if (coverageLines.length > 0) {
|
|
804
829
|
console.log("");
|
|
@@ -1590,6 +1615,7 @@ async function resolveReadParts(census, root) {
|
|
|
1590
1615
|
dark: {},
|
|
1591
1616
|
states: {},
|
|
1592
1617
|
skipped: [],
|
|
1618
|
+
unreadable: [],
|
|
1593
1619
|
literals: [],
|
|
1594
1620
|
fromToken: 0,
|
|
1595
1621
|
fromLiteral: 0,
|
|
@@ -527,6 +527,11 @@ function tokenRefFor(name) {
|
|
|
527
527
|
return `{spacing.${bare.slice(8)}}`;
|
|
528
528
|
if (bare.startsWith("shadow-"))
|
|
529
529
|
return `{shadow.${bare.slice(7)}}`;
|
|
530
|
+
if (bare.startsWith("background-image-gradient-")) {
|
|
531
|
+
return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
|
|
532
|
+
}
|
|
533
|
+
if (bare.startsWith("gradient-"))
|
|
534
|
+
return `{gradients.${bare.slice(9)}}`;
|
|
530
535
|
if (bare.startsWith("text-"))
|
|
531
536
|
return `{typography.scale.${bare.slice(5)}.fontSize}`;
|
|
532
537
|
if (bare.startsWith("font-"))
|
|
@@ -271,8 +271,22 @@ export function readUtility(utility, declared) {
|
|
|
271
271
|
const bracket = core.indexOf("-[");
|
|
272
272
|
if (bracket !== -1 && core.endsWith("]")) {
|
|
273
273
|
const head = core.slice(0, bracket);
|
|
274
|
-
|
|
274
|
+
let inner = core.slice(bracket + 2, -1);
|
|
275
|
+
/**
|
|
276
|
+
* `bg-[image:var(--gradient-ui)]` - the datatype hint says which property, in
|
|
277
|
+
* Tailwind's own syntax. Two real usages of the signature gradient wear exactly
|
|
278
|
+
* this spelling (dono, 01/08).
|
|
279
|
+
*/
|
|
280
|
+
let hinted = null;
|
|
281
|
+
const hint = /^(image|color|length|size|position|url):/.exec(inner);
|
|
282
|
+
if (hint) {
|
|
283
|
+
hinted = hint[1] === "image" ? "backgroundImage" : null;
|
|
284
|
+
inner = inner.slice(hint[0].length);
|
|
285
|
+
}
|
|
275
286
|
const value = resolveArbitrary(inner, declared);
|
|
287
|
+
if (hinted && value) {
|
|
288
|
+
return { property: hinted, value: value.value, token: value.token };
|
|
289
|
+
}
|
|
276
290
|
/**
|
|
277
291
|
* `text-[…]` IS AMBIGUOUS AND THE VALUE DECIDES.
|
|
278
292
|
*
|
|
@@ -299,6 +313,24 @@ export function readUtility(utility, declared) {
|
|
|
299
313
|
const rest = core.slice(dash + 1);
|
|
300
314
|
const colorProp = COLOR_PROPERTY[prefix];
|
|
301
315
|
if (colorProp) {
|
|
316
|
+
/**
|
|
317
|
+
* `bg-gradient-ui` is the utility Tailwind v4 mints from their
|
|
318
|
+
* `--background-image-gradient-ui`, and the colour path read `gradient-ui` as a
|
|
319
|
+
* colour name and found nothing. The token they declared is the answer - and
|
|
320
|
+
* `bg-gradient-to-r` must NOT land here, because nobody declares `--gradient-to-r`;
|
|
321
|
+
* it falls through to the stop composer below.
|
|
322
|
+
*/
|
|
323
|
+
if (prefix === "bg" && rest.startsWith("gradient-")) {
|
|
324
|
+
const name = rest.slice(9);
|
|
325
|
+
if (declared.has(`--background-image-gradient-${name}`) ||
|
|
326
|
+
declared.has(`--gradient-${name}`)) {
|
|
327
|
+
return {
|
|
328
|
+
property: "backgroundImage",
|
|
329
|
+
value: `{gradients.${name}}`,
|
|
330
|
+
token: `--gradient-${name}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
302
334
|
// Their own token first: `--color-ocean-500` for `bg-ocean-500`, which is
|
|
303
335
|
// exactly how Tailwind v4's `@theme` publishes it.
|
|
304
336
|
const own = `--color-${rest}`;
|
|
@@ -447,6 +479,22 @@ function resolveArbitrary(inner, declared) {
|
|
|
447
479
|
return null;
|
|
448
480
|
const v = /^var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)$/.exec(raw);
|
|
449
481
|
if (!v) {
|
|
482
|
+
/**
|
|
483
|
+
* `[--text-body-s]` IS the var() shorthand - Tailwind v3 defined it that way, so the
|
|
484
|
+
* author wrote a working thing once. v4 dropped the shorthand, which is why their
|
|
485
|
+
* inputs render at the inherited size today; the token they NAMED is still the
|
|
486
|
+
* intent, and a recipe is the rules to rebuild the component, not a photograph of
|
|
487
|
+
* the regression. Their own defect still gets reported - see `unreadable`.
|
|
488
|
+
*
|
|
489
|
+
* A bare `--name` they never declared resolves to nothing under either semantics,
|
|
490
|
+
* and carrying it would emit `font-size: --x`, which is invalid CSS.
|
|
491
|
+
*/
|
|
492
|
+
if (raw.startsWith("--")) {
|
|
493
|
+
const name = raw.split(",")[0].trim();
|
|
494
|
+
return declared.has(name)
|
|
495
|
+
? { value: tokenRefFor(name), token: name }
|
|
496
|
+
: null;
|
|
497
|
+
}
|
|
450
498
|
// A literal in brackets is still a real value somebody typed.
|
|
451
499
|
return /[<>{}@;]/.test(raw) ? null : { value: raw };
|
|
452
500
|
}
|
|
@@ -472,6 +520,14 @@ function tokenRefFor(name) {
|
|
|
472
520
|
return `{spacing.${bare.slice(8)}}`;
|
|
473
521
|
if (bare.startsWith("shadow-"))
|
|
474
522
|
return `{shadow.${bare.slice(7)}}`;
|
|
523
|
+
// `--gradient-ui` and `--background-image-gradient-ui` are one token in two
|
|
524
|
+
// spellings - Tailwind v4's utility namespace wraps the first. Both reach
|
|
525
|
+
// `{gradients.ui}`, which is where the census files them.
|
|
526
|
+
if (bare.startsWith("background-image-gradient-")) {
|
|
527
|
+
return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
|
|
528
|
+
}
|
|
529
|
+
if (bare.startsWith("gradient-"))
|
|
530
|
+
return `{gradients.${bare.slice(9)}}`;
|
|
475
531
|
if (bare.startsWith("text-")) {
|
|
476
532
|
return `{typography.scale.${bare.slice(5)}.fontSize}`;
|
|
477
533
|
}
|
|
@@ -540,18 +596,110 @@ export function readInlineStyle(source) {
|
|
|
540
596
|
}
|
|
541
597
|
/** Properties React leaves unitless, so a bare number is not pixels. */
|
|
542
598
|
const UNITLESS = /^(opacity|zIndex|flex|flexGrow|flexShrink|order|lineHeight|fontWeight|zoom|columnCount|fillOpacity|strokeOpacity|animationIterationCount)$/;
|
|
599
|
+
/**
|
|
600
|
+
* A prefix this reader claims to understand. A class that starts with one of these and
|
|
601
|
+
* still reads as nothing is a failed DESIGN read, not layout plumbing.
|
|
602
|
+
*/
|
|
603
|
+
const DESIGN_PREFIX = /^(?:p|px|py|pt|pr|pb|pl|m|mx|my|gap|rounded|text|font|bg|border|shadow|w|h|size|min-w|min-h|max-w|max-h|leading|tracking|opacity|fill|stroke)-\S/;
|
|
604
|
+
/** `to-r` → `to right`: Tailwind's eight directions, spelled as CSS reads them. */
|
|
605
|
+
const GRADIENT_DIRECTION = {
|
|
606
|
+
"to-t": "to top",
|
|
607
|
+
"to-tr": "to top right",
|
|
608
|
+
"to-r": "to right",
|
|
609
|
+
"to-br": "to bottom right",
|
|
610
|
+
"to-b": "to bottom",
|
|
611
|
+
"to-bl": "to bottom left",
|
|
612
|
+
"to-l": "to left",
|
|
613
|
+
"to-tl": "to top left",
|
|
614
|
+
};
|
|
615
|
+
/** A colour stop's value: their token first, Tailwind's table second, a literal last. */
|
|
616
|
+
function stopValue(raw, declared) {
|
|
617
|
+
if (declared.has(`--color-${raw}`))
|
|
618
|
+
return refFor(raw);
|
|
619
|
+
if (TAILWIND_COLOR[raw])
|
|
620
|
+
return TAILWIND_COLOR[raw];
|
|
621
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(raw) ||
|
|
622
|
+
raw === "transparent" ||
|
|
623
|
+
raw === "white" ||
|
|
624
|
+
raw === "black") {
|
|
625
|
+
return raw;
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* A COMPOSED GRADIENT is one decision spread across four classes.
|
|
631
|
+
*
|
|
632
|
+
* `bg-gradient-to-r from-ocean-500 via-vivid-pink-500 to-purple-400` is nine real
|
|
633
|
+
* elements in one repo, and every piece read as nothing - `from-` is not a property
|
|
634
|
+
* table's prefix. Composed here because only the whole class list can see all the
|
|
635
|
+
* pieces at once; the stops resolve to their own tokens, so the gradient re-themes.
|
|
636
|
+
*
|
|
637
|
+
* `from-bottom-2` taught the guard: `slide-in-from-bottom-2` fragments wear the same
|
|
638
|
+
* prefix, so stops only count when a `bg-gradient-to-*` sits in the SAME class list -
|
|
639
|
+
* and a direction whose stops do not resolve is reported, never half-built.
|
|
640
|
+
*/
|
|
641
|
+
function composeGradient(utilities, declared) {
|
|
642
|
+
const dir = utilities.find((u) => GRADIENT_DIRECTION[u.replace(/^bg-gradient-/, "")] &&
|
|
643
|
+
u.startsWith("bg-gradient-"));
|
|
644
|
+
if (!dir)
|
|
645
|
+
return { value: null, consumed: new Set(), broken: [] };
|
|
646
|
+
const direction = GRADIENT_DIRECTION[dir.replace(/^bg-gradient-/, "")];
|
|
647
|
+
const consumed = new Set([dir]);
|
|
648
|
+
const broken = [];
|
|
649
|
+
const stops = [];
|
|
650
|
+
for (const kind of ["from", "via", "to"]) {
|
|
651
|
+
const cls = utilities.find((u) => u.startsWith(`${kind}-`));
|
|
652
|
+
if (!cls)
|
|
653
|
+
continue;
|
|
654
|
+
consumed.add(cls);
|
|
655
|
+
const value = stopValue(cls.slice(kind.length + 1), declared);
|
|
656
|
+
if (value)
|
|
657
|
+
stops.push(value);
|
|
658
|
+
else
|
|
659
|
+
broken.push(cls);
|
|
660
|
+
}
|
|
661
|
+
if (stops.length < 2 || broken.length > 0) {
|
|
662
|
+
return {
|
|
663
|
+
value: null,
|
|
664
|
+
consumed,
|
|
665
|
+
broken: broken.length > 0 ? broken : [dir],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
value: `linear-gradient(${direction}, ${stops.join(", ")})`,
|
|
670
|
+
consumed,
|
|
671
|
+
broken,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
543
674
|
export function transcribe(classes, declared) {
|
|
544
675
|
const out = {
|
|
545
676
|
base: {},
|
|
546
677
|
states: {},
|
|
547
678
|
dark: {},
|
|
548
679
|
skipped: [],
|
|
680
|
+
unreadable: [],
|
|
549
681
|
literals: [],
|
|
550
682
|
fromToken: 0,
|
|
551
683
|
fromLiteral: 0,
|
|
552
684
|
};
|
|
685
|
+
/**
|
|
686
|
+
* The composed gradient first: it is one decision spread across four classes, and
|
|
687
|
+
* only the whole list sees all the pieces. Base-level only - a per-scheme stop
|
|
688
|
+
* (`dark:from-x`) is a condition this pass does not attempt, and it lands in
|
|
689
|
+
* `unreadable` below rather than half-composing.
|
|
690
|
+
*/
|
|
691
|
+
const bareUtilities = classes
|
|
692
|
+
.filter((cls) => parseClass(cls).modifiers.length === 0)
|
|
693
|
+
.map((cls) => parseClass(cls).utility);
|
|
694
|
+
const gradient = composeGradient(bareUtilities, declared);
|
|
695
|
+
if (gradient.value)
|
|
696
|
+
out.base.backgroundImage = gradient.value;
|
|
697
|
+
for (const broke of gradient.broken)
|
|
698
|
+
out.unreadable.push(broke);
|
|
553
699
|
for (const cls of classes) {
|
|
554
700
|
const { modifiers, utility } = parseClass(cls);
|
|
701
|
+
if (gradient.consumed.has(utility) && modifiers.length === 0)
|
|
702
|
+
continue;
|
|
555
703
|
const isDark = modifiers.includes("dark");
|
|
556
704
|
const rest = modifiers.filter((m) => m !== "dark");
|
|
557
705
|
// THE SLOT IS DECIDED BEFORE THE VALUE IS READ. Resolving first meant a
|
|
@@ -585,7 +733,16 @@ export function transcribe(classes, declared) {
|
|
|
585
733
|
out.skipped.push(cls);
|
|
586
734
|
continue;
|
|
587
735
|
}
|
|
588
|
-
if (!decl
|
|
736
|
+
if (!decl) {
|
|
737
|
+
// The prefix says design intent; the rest resolved to nothing. Malformed in their
|
|
738
|
+
// source more often than not - `p-spacing-sm` for `p-sm` - and a silent drop here
|
|
739
|
+
// is how a menu popup ships with no padding and nobody is told.
|
|
740
|
+
if (DESIGN_PREFIX.test(utility) && !out.unreadable.includes(cls)) {
|
|
741
|
+
out.unreadable.push(cls);
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (!target)
|
|
589
746
|
continue;
|
|
590
747
|
/**
|
|
591
748
|
* EVERYTHING IT READS TRAVELS, literals included - as DATA.
|
package/dist/skill-import.js
CHANGED
|
@@ -543,6 +543,20 @@ extensions are configured at construction, not toggled later
|
|
|
543
543
|
of their manifest and attaches it (\`"^2.1.0 in packages/ui"\`), so the rule does not go stale the
|
|
544
544
|
day they upgrade and the information is not lost either.
|
|
545
545
|
|
|
546
|
+
### If \`recipe_vocabulary\` is missing, the MCP registration is stale
|
|
547
|
+
|
|
548
|
+
A real run reported "recipe_vocabulary and validate_recipe do not exist on this server" and
|
|
549
|
+
sent every anatomy unvalidated - the repo's \`.mcp.json\` pinned \`synthesisui@0.16.78\` while
|
|
550
|
+
the import itself ran unpinned at latest: **two CLI versions in one repo, in one run** (dono,
|
|
551
|
+
01/08). Before concluding a tool does not exist, check the registration:
|
|
552
|
+
|
|
553
|
+
\`\`\`json
|
|
554
|
+
"args": ["synthesisui@latest", "mcp"]
|
|
555
|
+
\`\`\`
|
|
556
|
+
|
|
557
|
+
Tell the person to fix the pin and restart the server. Do not work around a stale server by
|
|
558
|
+
skipping validation - unvalidated is how correct work arrives as nothing.
|
|
559
|
+
|
|
546
560
|
### Ask what a recipe can hold, BEFORE you write one
|
|
547
561
|
|
|
548
562
|
\`\`\`
|
|
@@ -658,6 +672,10 @@ found them wastes a turn and risks contradicting the measurement:
|
|
|
658
672
|
- **whether a component forwards the rest of its props**, and which it names
|
|
659
673
|
- **which components appear inside which** at their call sites, as pairs
|
|
660
674
|
- **the folder architecture**, and it will ask you which one has priority
|
|
675
|
+
- **their gradients**, as tokens: \`--gradient-ui\` and its Tailwind-namespace twin land on
|
|
676
|
+
\`{gradients.ui}\`, composed \`bg-gradient-to-r from-x to-y\` stops become one gradient in
|
|
677
|
+
their own colour tokens, and a signature gradient is no longer a string trapped in class
|
|
678
|
+
names
|
|
661
679
|
- **the theme**: how many dark utilities, in how many files, which library switches it, which
|
|
662
680
|
attribute it is bound to, what \`<html>\` carries, and how many \`prefers-color-scheme\` rules
|
|
663
681
|
- **the libraries and their versions** - motion, icons, primitives, charts, editors - with the
|
|
@@ -669,6 +687,26 @@ found them wastes a turn and risks contradicting the measurement:
|
|
|
669
687
|
What is still yours: the anatomy tree, the part names, the frontiers, the roles, the
|
|
670
688
|
concept, and the rules that are not about a class name.
|
|
671
689
|
|
|
690
|
+
### A scale's INTENT is a rule, and their docs already wrote it
|
|
691
|
+
|
|
692
|
+
A three-step spacing scale with documented meaning - sm "close UI relationships", md
|
|
693
|
+
"standard internal padding", lg "layout gutters" - carries a law nothing else expresses:
|
|
694
|
+
**this system has exactly three steps, and a fourth is a mistake.** A real import read the
|
|
695
|
+
scale and lost the sentence (dono, 01/08).
|
|
696
|
+
|
|
697
|
+
When their docs (a \`Geometry.mdx\`, a design-tokens page, comments beside the \`@theme\`)
|
|
698
|
+
state what a step is FOR, send it as system-wide rules - \`applies: []\`, \`fact: true\`, the
|
|
699
|
+
doc as evidence:
|
|
700
|
+
|
|
701
|
+
\`\`\`
|
|
702
|
+
this system's spacing has exactly three steps - sm for close relationships inside a
|
|
703
|
+
control, md for standard internal padding, lg for layout gutters. A fourth step is a
|
|
704
|
+
mistake, not a gap (Geometry.mdx)
|
|
705
|
+
\`\`\`
|
|
706
|
+
|
|
707
|
+
\`applies: []\` is what makes it reach CLAUDE.md and every prompt after - the same route the
|
|
708
|
+
architecture rule travels.
|
|
709
|
+
|
|
672
710
|
### How much to send
|
|
673
711
|
|
|
674
712
|
Send an anatomy for **every component with visible structure**, which is nearly all of them. A
|
package/package.json
CHANGED