synthesisui 0.16.87 → 0.16.89

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.
@@ -288,7 +288,21 @@ async function validateRecipe(name, recipe) {
288
288
  const answer = await askCatalogue("validate", { name, recipe });
289
289
  if (!answer.ok)
290
290
  return answer.because;
291
- const v = answer.body;
291
+ const raw = answer.body;
292
+ /**
293
+ * THE WIRE IS NOT A TYPE SYSTEM. The schema-refusal branch of the API answered
294
+ * without a `checklist`, and this client read `v.checklist.length` unguarded - so the
295
+ * one recipe that most needed the validator crashed it, on every call, and a real
296
+ * import had to validate against the API by hand (test13, 01/08). Every field is
297
+ * defaulted; a shape this cannot read degrades to a sentence, never to a throw.
298
+ */
299
+ const v = {
300
+ ok: raw.ok === true,
301
+ rejected: raw.rejected ?? [],
302
+ lost: raw.lost ?? [],
303
+ missing: raw.missing ?? [],
304
+ checklist: raw.checklist ?? [],
305
+ };
292
306
  if (v.ok) {
293
307
  return `${name}: everything you sent would survive. Send it.`;
294
308
  }
@@ -527,8 +527,27 @@ 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("text-"))
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)}}`;
535
+ if (bare.startsWith("text-")) {
536
+ // The companion token: `--text-body-s--line-height` names the line-height OF a
537
+ // step, and a double dash inside a ref is a grammar nothing accepts (test13).
538
+ const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
539
+ if (companion) {
540
+ const prop = {
541
+ "line-height": "lineHeight",
542
+ "letter-spacing": "letterSpacing",
543
+ "font-weight": "weight",
544
+ }[companion[2]];
545
+ return `{typography.scale.${companion[1]}.${prop}}`;
546
+ }
547
+ if (bare.slice(5).includes("--"))
548
+ return `var(${name})`;
531
549
  return `{typography.scale.${bare.slice(5)}.fontSize}`;
550
+ }
532
551
  if (bare.startsWith("font-"))
533
552
  return `{typography.families.${bare.slice(5)}}`;
534
553
  return `var(${name})`;
@@ -190,8 +190,37 @@ const TYPE_KEYWORD = {
190
190
  underline: { property: "textDecoration", value: "underline" },
191
191
  };
192
192
  /** States we recognise. Anything else is skipped rather than invented. */
193
- const STATE = /^(hover|focus|focus-visible|active|disabled|checked)$/;
194
- const DATA_STATE = /^data-\[([a-z-]+)\]$/;
193
+ /**
194
+ * A MODIFIER'S STATE, IN THE CONTRACT'S SPELLING.
195
+ *
196
+ * The old regex KEYED the state with the raw modifier, so `focus-visible:ring-2` arrived
197
+ * as `states["focus-visible"]` - a key the compiler cannot spell and the lint rejects.
198
+ * A real import hit it on Modal's close button and had to hand-patch the census before
199
+ * sending (test13, 01/08). The reading edge normalizes; the contract stays canonical.
200
+ */
201
+ const STATE_SPELLING = {
202
+ hover: "hover",
203
+ focus: "focus",
204
+ "focus-visible": "focusVisible",
205
+ "focus-within": "focus",
206
+ active: "active",
207
+ disabled: "disabled",
208
+ checked: "checked",
209
+ selected: "selected",
210
+ open: "open",
211
+ invalid: "invalid",
212
+ loading: "loading",
213
+ "read-only": "readOnly",
214
+ required: "required",
215
+ placeholder: "placeholder",
216
+ highlighted: "highlighted",
217
+ even: "even",
218
+ odd: "odd",
219
+ first: "first",
220
+ last: "last",
221
+ empty: "empty",
222
+ };
223
+ const DATA_STATE = /^data-\[(?:state=)?([a-z-]+)\]$/;
195
224
  /**
196
225
  * Split a class into its modifiers and the utility itself.
197
226
  *
@@ -271,8 +300,22 @@ export function readUtility(utility, declared) {
271
300
  const bracket = core.indexOf("-[");
272
301
  if (bracket !== -1 && core.endsWith("]")) {
273
302
  const head = core.slice(0, bracket);
274
- const inner = core.slice(bracket + 2, -1);
303
+ let inner = core.slice(bracket + 2, -1);
304
+ /**
305
+ * `bg-[image:var(--gradient-ui)]` - the datatype hint says which property, in
306
+ * Tailwind's own syntax. Two real usages of the signature gradient wear exactly
307
+ * this spelling (dono, 01/08).
308
+ */
309
+ let hinted = null;
310
+ const hint = /^(image|color|length|size|position|url):/.exec(inner);
311
+ if (hint) {
312
+ hinted = hint[1] === "image" ? "backgroundImage" : null;
313
+ inner = inner.slice(hint[0].length);
314
+ }
275
315
  const value = resolveArbitrary(inner, declared);
316
+ if (hinted && value) {
317
+ return { property: hinted, value: value.value, token: value.token };
318
+ }
276
319
  /**
277
320
  * `text-[…]` IS AMBIGUOUS AND THE VALUE DECIDES.
278
321
  *
@@ -299,6 +342,24 @@ export function readUtility(utility, declared) {
299
342
  const rest = core.slice(dash + 1);
300
343
  const colorProp = COLOR_PROPERTY[prefix];
301
344
  if (colorProp) {
345
+ /**
346
+ * `bg-gradient-ui` is the utility Tailwind v4 mints from their
347
+ * `--background-image-gradient-ui`, and the colour path read `gradient-ui` as a
348
+ * colour name and found nothing. The token they declared is the answer - and
349
+ * `bg-gradient-to-r` must NOT land here, because nobody declares `--gradient-to-r`;
350
+ * it falls through to the stop composer below.
351
+ */
352
+ if (prefix === "bg" && rest.startsWith("gradient-")) {
353
+ const name = rest.slice(9);
354
+ if (declared.has(`--background-image-gradient-${name}`) ||
355
+ declared.has(`--gradient-${name}`)) {
356
+ return {
357
+ property: "backgroundImage",
358
+ value: `{gradients.${name}}`,
359
+ token: `--gradient-${name}`,
360
+ };
361
+ }
362
+ }
302
363
  // Their own token first: `--color-ocean-500` for `bg-ocean-500`, which is
303
364
  // exactly how Tailwind v4's `@theme` publishes it.
304
365
  const own = `--color-${rest}`;
@@ -488,7 +549,35 @@ function tokenRefFor(name) {
488
549
  return `{spacing.${bare.slice(8)}}`;
489
550
  if (bare.startsWith("shadow-"))
490
551
  return `{shadow.${bare.slice(7)}}`;
552
+ // `--gradient-ui` and `--background-image-gradient-ui` are one token in two
553
+ // spellings - Tailwind v4's utility namespace wraps the first. Both reach
554
+ // `{gradients.ui}`, which is where the census files them.
555
+ if (bare.startsWith("background-image-gradient-")) {
556
+ return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
557
+ }
558
+ if (bare.startsWith("gradient-"))
559
+ return `{gradients.${bare.slice(9)}}`;
491
560
  if (bare.startsWith("text-")) {
561
+ /**
562
+ * THE COMPANION TOKEN. Tailwind v4 spells "the line-height OF text-body-s" as
563
+ * `--text-body-s--line-height` - a double dash inside one name. Read as a step name
564
+ * it produced `{typography.scale.body-s--line-height.fontSize}`, whose double dash
565
+ * no ref grammar accepts, and the whole recipe was REFUSED at validation (test13,
566
+ * 01/08). The suffix names the property; the middle names the step.
567
+ */
568
+ const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
569
+ if (companion) {
570
+ const prop = {
571
+ "line-height": "lineHeight",
572
+ "letter-spacing": "letterSpacing",
573
+ "font-weight": "weight",
574
+ }[companion[2]];
575
+ return `{typography.scale.${companion[1]}.${prop}}`;
576
+ }
577
+ // Any other double dash is a name this grammar cannot hold - the literal var()
578
+ // still resolves against their own stylesheet, and an invalid ref helps nobody.
579
+ if (bare.slice(5).includes("--"))
580
+ return `var(${name})`;
492
581
  return `{typography.scale.${bare.slice(5)}.fontSize}`;
493
582
  }
494
583
  if (bare.startsWith("font-"))
@@ -561,6 +650,76 @@ const UNITLESS = /^(opacity|zIndex|flex|flexGrow|flexShrink|order|lineHeight|fon
561
650
  * still reads as nothing is a failed DESIGN read, not layout plumbing.
562
651
  */
563
652
  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/;
653
+ /** `to-r` → `to right`: Tailwind's eight directions, spelled as CSS reads them. */
654
+ const GRADIENT_DIRECTION = {
655
+ "to-t": "to top",
656
+ "to-tr": "to top right",
657
+ "to-r": "to right",
658
+ "to-br": "to bottom right",
659
+ "to-b": "to bottom",
660
+ "to-bl": "to bottom left",
661
+ "to-l": "to left",
662
+ "to-tl": "to top left",
663
+ };
664
+ /** A colour stop's value: their token first, Tailwind's table second, a literal last. */
665
+ function stopValue(raw, declared) {
666
+ if (declared.has(`--color-${raw}`))
667
+ return refFor(raw);
668
+ if (TAILWIND_COLOR[raw])
669
+ return TAILWIND_COLOR[raw];
670
+ if (/^#[0-9a-fA-F]{3,8}$/.test(raw) ||
671
+ raw === "transparent" ||
672
+ raw === "white" ||
673
+ raw === "black") {
674
+ return raw;
675
+ }
676
+ return null;
677
+ }
678
+ /**
679
+ * A COMPOSED GRADIENT is one decision spread across four classes.
680
+ *
681
+ * `bg-gradient-to-r from-ocean-500 via-vivid-pink-500 to-purple-400` is nine real
682
+ * elements in one repo, and every piece read as nothing - `from-` is not a property
683
+ * table's prefix. Composed here because only the whole class list can see all the
684
+ * pieces at once; the stops resolve to their own tokens, so the gradient re-themes.
685
+ *
686
+ * `from-bottom-2` taught the guard: `slide-in-from-bottom-2` fragments wear the same
687
+ * prefix, so stops only count when a `bg-gradient-to-*` sits in the SAME class list -
688
+ * and a direction whose stops do not resolve is reported, never half-built.
689
+ */
690
+ function composeGradient(utilities, declared) {
691
+ const dir = utilities.find((u) => GRADIENT_DIRECTION[u.replace(/^bg-gradient-/, "")] &&
692
+ u.startsWith("bg-gradient-"));
693
+ if (!dir)
694
+ return { value: null, consumed: new Set(), broken: [] };
695
+ const direction = GRADIENT_DIRECTION[dir.replace(/^bg-gradient-/, "")];
696
+ const consumed = new Set([dir]);
697
+ const broken = [];
698
+ const stops = [];
699
+ for (const kind of ["from", "via", "to"]) {
700
+ const cls = utilities.find((u) => u.startsWith(`${kind}-`));
701
+ if (!cls)
702
+ continue;
703
+ consumed.add(cls);
704
+ const value = stopValue(cls.slice(kind.length + 1), declared);
705
+ if (value)
706
+ stops.push(value);
707
+ else
708
+ broken.push(cls);
709
+ }
710
+ if (stops.length < 2 || broken.length > 0) {
711
+ return {
712
+ value: null,
713
+ consumed,
714
+ broken: broken.length > 0 ? broken : [dir],
715
+ };
716
+ }
717
+ return {
718
+ value: `linear-gradient(${direction}, ${stops.join(", ")})`,
719
+ consumed,
720
+ broken,
721
+ };
722
+ }
564
723
  export function transcribe(classes, declared) {
565
724
  const out = {
566
725
  base: {},
@@ -572,8 +731,24 @@ export function transcribe(classes, declared) {
572
731
  fromToken: 0,
573
732
  fromLiteral: 0,
574
733
  };
734
+ /**
735
+ * The composed gradient first: it is one decision spread across four classes, and
736
+ * only the whole list sees all the pieces. Base-level only - a per-scheme stop
737
+ * (`dark:from-x`) is a condition this pass does not attempt, and it lands in
738
+ * `unreadable` below rather than half-composing.
739
+ */
740
+ const bareUtilities = classes
741
+ .filter((cls) => parseClass(cls).modifiers.length === 0)
742
+ .map((cls) => parseClass(cls).utility);
743
+ const gradient = composeGradient(bareUtilities, declared);
744
+ if (gradient.value)
745
+ out.base.backgroundImage = gradient.value;
746
+ for (const broke of gradient.broken)
747
+ out.unreadable.push(broke);
575
748
  for (const cls of classes) {
576
749
  const { modifiers, utility } = parseClass(cls);
750
+ if (gradient.consumed.has(utility) && modifiers.length === 0)
751
+ continue;
577
752
  const isDark = modifiers.includes("dark");
578
753
  const rest = modifiers.filter((m) => m !== "dark");
579
754
  // THE SLOT IS DECIDED BEFORE THE VALUE IS READ. Resolving first meant a
@@ -586,9 +761,10 @@ export function transcribe(classes, declared) {
586
761
  target = isDark ? out.dark : out.base;
587
762
  }
588
763
  else if (rest.length === 1 && !isDark) {
589
- const state = STATE.test(rest[0])
764
+ const raw = STATE_SPELLING[rest[0]] != null
590
765
  ? rest[0]
591
766
  : (DATA_STATE.exec(rest[0])?.[1] ?? null);
767
+ const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
592
768
  if (state)
593
769
  target = out.states[state] ?? (out.states[state] = {});
594
770
  else
@@ -635,6 +811,23 @@ export function transcribe(classes, declared) {
635
811
  // First write wins, matching how a class list resolves for the properties
636
812
  // we read: later duplicates in the same list are almost always a merge
637
813
  // artefact rather than an override.
814
+ /**
815
+ * `size-4` is Tailwind for width AND height; CSS has no `size` property for
816
+ * elements, so the platform dropped the declaration whole and a real import had to
817
+ * rewrite every one by hand as `w-N h-N` (test13, 01/08). One utility, two
818
+ * declarations, on whichever target the modifiers routed to.
819
+ */
820
+ if (decl.property === "size") {
821
+ if (target.width == null)
822
+ target.width = decl.value;
823
+ if (target.height == null)
824
+ target.height = decl.value;
825
+ if (decl.token)
826
+ out.fromToken += 1;
827
+ else
828
+ out.fromLiteral += 1;
829
+ continue;
830
+ }
638
831
  if (target[decl.property] != null)
639
832
  continue;
640
833
  target[decl.property] = decl.value;
@@ -49,6 +49,7 @@ const MODIFIER_STATE = {
49
49
  checked: "checked",
50
50
  indeterminate: "checked",
51
51
  selected: "selected",
52
+ highlighted: "highlighted",
52
53
  open: "open",
53
54
  invalid: "invalid",
54
55
  required: "required",
@@ -672,6 +672,10 @@ found them wastes a turn and risks contradicting the measurement:
672
672
  - **whether a component forwards the rest of its props**, and which it names
673
673
  - **which components appear inside which** at their call sites, as pairs
674
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
675
679
  - **the theme**: how many dark utilities, in how many files, which library switches it, which
676
680
  attribute it is bound to, what \`<html>\` carries, and how many \`prefers-color-scheme\` rules
677
681
  - **the libraries and their versions** - motion, icons, primitives, charts, editors - with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.87",
3
+ "version": "0.16.89",
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": {