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.
@@ -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() : "";
@@ -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
- const inner = core.slice(bracket + 2, -1);
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 || !target)
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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.86",
3
+ "version": "0.16.88",
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": {