synthesisui 0.16.88 → 0.16.90
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 +15 -1
- package/dist/doctor/signals.js +71 -0
- package/dist/doctor/transcribe.js +70 -3
- package/dist/doctor/variant-read.js +1 -0
- package/dist/skill-import.js +18 -1
- 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
|
}
|
|
@@ -532,8 +532,22 @@ function tokenRefFor(name) {
|
|
|
532
532
|
}
|
|
533
533
|
if (bare.startsWith("gradient-"))
|
|
534
534
|
return `{gradients.${bare.slice(9)}}`;
|
|
535
|
-
if (bare.startsWith("text-"))
|
|
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})`;
|
|
536
549
|
return `{typography.scale.${bare.slice(5)}.fontSize}`;
|
|
550
|
+
}
|
|
537
551
|
if (bare.startsWith("font-"))
|
|
538
552
|
return `{typography.families.${bare.slice(5)}}`;
|
|
539
553
|
return `var(${name})`;
|
package/dist/doctor/signals.js
CHANGED
|
@@ -75,11 +75,19 @@ export function emptySignals() {
|
|
|
75
75
|
enableSystem: null,
|
|
76
76
|
prefersColorScheme: 0,
|
|
77
77
|
htmlCarries: [],
|
|
78
|
+
page: [],
|
|
78
79
|
},
|
|
79
80
|
libraries: [],
|
|
80
81
|
conflicts: [],
|
|
81
82
|
};
|
|
82
83
|
}
|
|
84
|
+
function addPagePaint(into, token, scheme, source) {
|
|
85
|
+
const found = into.theme.page.find((p) => p.token === token && p.scheme === scheme && p.source === source);
|
|
86
|
+
if (found)
|
|
87
|
+
found.count += 1;
|
|
88
|
+
else
|
|
89
|
+
into.theme.page.push({ token, scheme, source, count: 1 });
|
|
90
|
+
}
|
|
83
91
|
const ANY_IMPORT = /(?:from\s+["']([^"'.][^"']*)["']|require\(\s*["']([^"'.][^"']*)["']\s*\))/g;
|
|
84
92
|
/** The package a specifier belongs to: `@tiptap/react` and `motion/react` both keep the
|
|
85
93
|
* part a manifest would name. */
|
|
@@ -110,6 +118,13 @@ export function readSignalsInto(into, imports, file, source) {
|
|
|
110
118
|
const isStyle = /\.(css|scss|sass|less)$/i.test(file);
|
|
111
119
|
if (isStyle) {
|
|
112
120
|
into.theme.prefersColorScheme += (source.match(/prefers-color-scheme/g) ?? []).length;
|
|
121
|
+
/**
|
|
122
|
+
* `body { background: … }` in a global stylesheet is the page saying so itself -
|
|
123
|
+
* the strongest of the three page signals and the one a class sweep never finds.
|
|
124
|
+
*/
|
|
125
|
+
for (const m of source.matchAll(/(?:^|\})\s*(?:html|body|:root)[^{]*\{[^}]*?background(?:-color)?\s*:\s*([^;}]+)/g)) {
|
|
126
|
+
addPagePaint(into, m[1].trim(), "light", "global-css");
|
|
127
|
+
}
|
|
113
128
|
return;
|
|
114
129
|
}
|
|
115
130
|
if (!isCode)
|
|
@@ -152,6 +167,37 @@ export function readSignalsInto(into, imports, file, source) {
|
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* THE PAGE PAINTS, from the markup. A `min-h-screen` container with a background is
|
|
172
|
+
* the page - that idiom is universal, which is what makes this generalize across
|
|
173
|
+
* folder layouts. `dark:bg-x` files under dark; `bg-x` under light.
|
|
174
|
+
*/
|
|
175
|
+
for (const m of source.matchAll(/className=\{?["'`]([^"'`]+)["'`]/g)) {
|
|
176
|
+
const list = m[1].split(/\s+/);
|
|
177
|
+
const isViewport = list.some((c) => /^(?:min-h-screen|h-screen|min-h-dvh|h-dvh)$/.test(c));
|
|
178
|
+
if (isViewport) {
|
|
179
|
+
for (const c of list) {
|
|
180
|
+
const dark = /^dark:bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
181
|
+
const light = /^bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
182
|
+
if (dark)
|
|
183
|
+
addPagePaint(into, dark[1], "dark", "viewport-container");
|
|
184
|
+
else if (light &&
|
|
185
|
+
!/^(gradient|opacity|clip|cover|contain|center|fixed|local|scroll|none|auto|repeat|transparent|current|inherit)/.test(light[1])) {
|
|
186
|
+
addPagePaint(into, light[1], "light", "viewport-container");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const m of source.matchAll(/<(?:main|body)\b[^>]*className=\{?["'`]([^"'`]+)["'`]/g)) {
|
|
192
|
+
for (const c of m[1].split(/\s+/)) {
|
|
193
|
+
const dark = /^dark:bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
194
|
+
const light = /^bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
195
|
+
if (dark)
|
|
196
|
+
addPagePaint(into, dark[1], "dark", "main-or-body");
|
|
197
|
+
else if (light)
|
|
198
|
+
addPagePaint(into, light[1], "light", "main-or-body");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
155
201
|
ANY_IMPORT.lastIndex = 0;
|
|
156
202
|
const seen = new Set();
|
|
157
203
|
for (const m of source.matchAll(ANY_IMPORT)) {
|
|
@@ -267,6 +313,31 @@ export function describeSignals(s) {
|
|
|
267
313
|
if (t.htmlCarries.length > 0) {
|
|
268
314
|
out.push(`\`<html>\` carries ${t.htmlCarries.map((a) => `\`${a}\``).join(" ")} - which is the ancestor every dark rule of theirs matches, and therefore the one ours must match.`);
|
|
269
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* The page, said as evidence with the SOURCE named - "2 full-viewport containers"
|
|
318
|
+
* is a claim somebody can weigh against "1 main". Strongest first per scheme.
|
|
319
|
+
*/
|
|
320
|
+
const strength = {
|
|
321
|
+
"global-css": 3,
|
|
322
|
+
"main-or-body": 2,
|
|
323
|
+
"viewport-container": 1,
|
|
324
|
+
};
|
|
325
|
+
for (const scheme of ["dark", "light"]) {
|
|
326
|
+
const paints = t.page
|
|
327
|
+
.filter((p) => p.scheme === scheme)
|
|
328
|
+
.sort((a, b) => strength[b.source] - strength[a.source] || b.count - a.count);
|
|
329
|
+
if (paints.length === 0)
|
|
330
|
+
continue;
|
|
331
|
+
const label = {
|
|
332
|
+
"global-css": "body in global CSS",
|
|
333
|
+
"main-or-body": "on <main>/<body>",
|
|
334
|
+
"viewport-container": "full-viewport container",
|
|
335
|
+
};
|
|
336
|
+
out.push(`the ${scheme} page paints ${paints
|
|
337
|
+
.slice(0, 3)
|
|
338
|
+
.map((p) => `\`${p.token}\` (${p.count}x ${label[p.source]})`)
|
|
339
|
+
.join(", ")} - the canvas question, answered from the files rather than hunted.`);
|
|
340
|
+
}
|
|
270
341
|
for (const family of ["primitives", "motion", "icons", "charts", "editors"]) {
|
|
271
342
|
const found = s.libraries.filter((l) => l.family === family);
|
|
272
343
|
if (found.length === 0)
|
|
@@ -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
|
*
|
|
@@ -529,6 +558,26 @@ function tokenRefFor(name) {
|
|
|
529
558
|
if (bare.startsWith("gradient-"))
|
|
530
559
|
return `{gradients.${bare.slice(9)}}`;
|
|
531
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})`;
|
|
532
581
|
return `{typography.scale.${bare.slice(5)}.fontSize}`;
|
|
533
582
|
}
|
|
534
583
|
if (bare.startsWith("font-"))
|
|
@@ -712,9 +761,10 @@ export function transcribe(classes, declared) {
|
|
|
712
761
|
target = isDark ? out.dark : out.base;
|
|
713
762
|
}
|
|
714
763
|
else if (rest.length === 1 && !isDark) {
|
|
715
|
-
const
|
|
764
|
+
const raw = STATE_SPELLING[rest[0]] != null
|
|
716
765
|
? rest[0]
|
|
717
766
|
: (DATA_STATE.exec(rest[0])?.[1] ?? null);
|
|
767
|
+
const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
|
|
718
768
|
if (state)
|
|
719
769
|
target = out.states[state] ?? (out.states[state] = {});
|
|
720
770
|
else
|
|
@@ -761,6 +811,23 @@ export function transcribe(classes, declared) {
|
|
|
761
811
|
// First write wins, matching how a class list resolves for the properties
|
|
762
812
|
// we read: later duplicates in the same list are almost always a merge
|
|
763
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
|
+
}
|
|
764
831
|
if (target[decl.property] != null)
|
|
765
832
|
continue;
|
|
766
833
|
target[decl.property] = decl.value;
|
package/dist/skill-import.js
CHANGED
|
@@ -251,12 +251,29 @@ html carries lang="en" data-theme="dark" - which is the ancestor every dark rule
|
|
|
251
251
|
That is a real run. Your job is to CONFIRM OR CORRECT it, not to reproduce it - and a sweep
|
|
252
252
|
would bring back a different slice than the next one, which is the worse property of the two.
|
|
253
253
|
|
|
254
|
+
**Confirmation costs ZERO reads.** A real run spent 7 pattern searches and 39 file reads to
|
|
255
|
+
"confirm" a theme the census had already counted (test14, 01/08) - that is re-deriving, not
|
|
256
|
+
confirming. The rule: the census numbers are TRUE unless something you already read while
|
|
257
|
+
doing other work contradicts them. Only on a contradiction do you open a file, and only the
|
|
258
|
+
one file the evidence names. If nothing contradicts, write the census's answer down and move
|
|
259
|
+
on - it costs one sentence.
|
|
260
|
+
|
|
254
261
|
\`has: ["dark"]\` on a dark-only product is the right answer. It is better than filling a light
|
|
255
262
|
slot on their behalf, and it makes adding light a deliberate act they take later.
|
|
256
263
|
|
|
257
264
|
**\`roles\` - which of their values carries which meaning**, when their token names do not say.
|
|
258
265
|
A project naming its neutrals by temperature (\`--color-darkgray-900\`) has told you nothing
|
|
259
|
-
about which one is the page
|
|
266
|
+
about which one is the page - and the census now answers that too, under "the page paints":
|
|
267
|
+
|
|
268
|
+
\`\`\`
|
|
269
|
+
the dark page paints darkgray-500 (2x on <main>/<body>, 2x full-viewport container)
|
|
270
|
+
the light page paints #EDF3FA (1x body in global CSS), lightgray-200 (2x viewport)
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
273
|
+
Three countable sources, strongest first: \`body { background }\` in global CSS, a background
|
|
274
|
+
on \`<main>\`/\`<body>\`, and a full-viewport container (\`min-h-screen\` + a background IS the
|
|
275
|
+
page). **Do not hunt for the canvas** - pick from this evidence, and name the source when you
|
|
276
|
+
report it. When the sources disagree, that disagreement is itself the finding to surface.
|
|
260
277
|
|
|
261
278
|
**\`fonts\` and \`concept\`** - the voice, and one paragraph on what this product is. The concept
|
|
262
279
|
feeds every recommendation downstream, so a real one beats a generic one by a wide margin.
|
package/package.json
CHANGED