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.
- package/dist/commands/mcp.js +15 -1
- package/dist/doctor/css-modules.js +20 -1
- package/dist/doctor/transcribe.js +197 -4
- package/dist/doctor/variant-read.js +1 -0
- package/dist/skill-import.js +4 -0
- package/package.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
|
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("
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/skill-import.js
CHANGED
|
@@ -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