synthesisui 0.16.26 → 0.16.28
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/doctor.js +1 -0
- package/dist/doctor/scan.js +91 -0
- package/dist/doctor/tokens.js +11 -0
- package/dist/guide.js +13 -4
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -191,6 +191,7 @@ const KIND_LABEL = {
|
|
|
191
191
|
radius: "radius",
|
|
192
192
|
spacing: "spacing",
|
|
193
193
|
font: "type",
|
|
194
|
+
motion: "motion",
|
|
194
195
|
};
|
|
195
196
|
/** "1 file", "2 files" - a report that says "1 files read" on its very first
|
|
196
197
|
* line spends credibility before it has said anything. */
|
package/dist/doctor/scan.js
CHANGED
|
@@ -59,6 +59,34 @@ const RADIUS = new RegExp(`(?:border-?[Rr]adius\\s*:\\s*${OPEN}|rounded(?:-[a-z]
|
|
|
59
59
|
const SPACING = new RegExp(`(?:\\b[pmg](?:[trblxy])?-\\[|gap-\\[|(?:padding|margin|gap)(?:[A-Z][a-z]+)?\\s*:\\s*${OPEN})(-?\\d*\\.?\\d+)(px|rem)`, "g");
|
|
60
60
|
/** A font stack written by hand rather than taken from the type scale. */
|
|
61
61
|
const FONT = /font-family\s*:\s*([^;}\n]+)/g;
|
|
62
|
+
/**
|
|
63
|
+
* MOTION, the family the scan never read (item 12, F3).
|
|
64
|
+
*
|
|
65
|
+
* A transition or animation declaration carrying a literal clock - `300ms`,
|
|
66
|
+
* `.2s`, a hand-tuned `cubic-bezier` - is timing decided outside the system,
|
|
67
|
+
* exactly like a hex outside the palette. Anchored to the property so a
|
|
68
|
+
* number elsewhere on the line (a scale factor, a z-index) is never read as
|
|
69
|
+
* time; the value normalizer makes `.3s` and `300ms` one literal, so the
|
|
70
|
+
* system's own duration token gets NAMED, not just flagged.
|
|
71
|
+
*/
|
|
72
|
+
// `]` ends the value too: Tailwind's arbitrary syntax (`[animation:...]`)
|
|
73
|
+
// closes with it, and without the stop the "value" swallowed the rest of the
|
|
74
|
+
// className - first real report blamed classes that sat beside the animation.
|
|
75
|
+
const MOTION_PROP = /\b(?:transition|animation)(?:-(?:duration|delay|timing-function)|Duration|Delay|TimingFunction)?\s*:\s*["'`]?([^;}\]"'`\n]*)/g;
|
|
76
|
+
// Trailing guard instead of `\b`: in arbitrary syntax spaces are `_`, which is
|
|
77
|
+
// a word char, so `200ms_ease` failed `\b` and the hand-written clock passed
|
|
78
|
+
// unseen (found on the generated button.tsx, first field run of the family).
|
|
79
|
+
const TIME_LITERAL = /(\d*\.?\d+)(ms|s)(?![a-zA-Z0-9%])/g;
|
|
80
|
+
const BEZIER = /cubic-bezier\([^)]*\)/g;
|
|
81
|
+
/** `duration-300`, `duration-[350ms]`, `delay-75` - Tailwind's stock clock. */
|
|
82
|
+
const TW_TIME = /\b(?:duration|delay)-(?:\[([^\]]+)\]|(\d+)\b)/g;
|
|
83
|
+
const TW_EASE = /\bease-\[([^\]]+)\]/g;
|
|
84
|
+
/** `animate-fade-up`, `animate-[wiggle_1s_ease]` - checked against the
|
|
85
|
+
* installed vocabulary, so only when the system ships one. */
|
|
86
|
+
const ANIMATE_UTIL = /\banimate-(\[[^\]]+\]|[a-z0-9-]+)/g;
|
|
87
|
+
/** tailwindcss-animate's grammar (the shadcn bridge): `animate-in`/`-out` are
|
|
88
|
+
* composable micro-transition idiom, not a keyframe selection to police. */
|
|
89
|
+
const ANIMATE_IDIOM = new Set(["none", "in", "out"]);
|
|
62
90
|
/**
|
|
63
91
|
* Uses of the system. Coverage is meaningless without them.
|
|
64
92
|
*
|
|
@@ -308,6 +336,68 @@ export function scanSource(file, source, table) {
|
|
|
308
336
|
continue;
|
|
309
337
|
push("font", stack);
|
|
310
338
|
}
|
|
339
|
+
for (const m of line.matchAll(MOTION_PROP)) {
|
|
340
|
+
const value = m[1];
|
|
341
|
+
const base = (m.index ?? 0) + m[0].length - value.length;
|
|
342
|
+
for (const t of value.matchAll(TIME_LITERAL)) {
|
|
343
|
+
// `0s` is idiom (disabling a transition), not a clock chosen by hand.
|
|
344
|
+
if (Number.parseFloat(t[1]) === 0)
|
|
345
|
+
continue;
|
|
346
|
+
push("motion", `${t[1]}${t[2]}`, base + (t.index ?? 0));
|
|
347
|
+
}
|
|
348
|
+
for (const b of value.matchAll(BEZIER)) {
|
|
349
|
+
push("motion", b[0], base + (b.index ?? 0));
|
|
350
|
+
}
|
|
351
|
+
// A `ds-*` animation name that the installed css never declares renders
|
|
352
|
+
// as NOTHING, silently - the keyframe cousin of a phantom token.
|
|
353
|
+
//
|
|
354
|
+
// Two guards, both from the first field run (soft-test, 29/07): the
|
|
355
|
+
// lookbehind keeps `var(--ds-motion-durations-base)` from reading as a
|
|
356
|
+
// keyframe named after the token (`-` before `ds-` means it is a custom
|
|
357
|
+
// property, not a name); and the name must END on a word - the greedy
|
|
358
|
+
// run used to stop at Tailwind's `_` separator and report a truncated
|
|
359
|
+
// `ds-soft-test-` as phantom while the real keyframe existed.
|
|
360
|
+
if (table.keyframes.size > 0) {
|
|
361
|
+
for (const w of value.matchAll(/(?<![\w-])(ds-[a-z0-9]+(?:-[a-z0-9]+)*)/g)) {
|
|
362
|
+
if (!table.keyframes.has(w[1]))
|
|
363
|
+
phantoms.push({ name: `@keyframes ${w[1]}`, line: at });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
for (const m of line.matchAll(TW_TIME)) {
|
|
368
|
+
const literal = m[1] ?? `${m[2]}ms`;
|
|
369
|
+
// `duration-[var(--ds-motion-durations-base)]` is the token being USED.
|
|
370
|
+
if (literal.startsWith("var("))
|
|
371
|
+
continue;
|
|
372
|
+
if (Number.parseFloat(literal) === 0)
|
|
373
|
+
continue;
|
|
374
|
+
push("motion", literal, m.index ?? -1);
|
|
375
|
+
}
|
|
376
|
+
for (const m of line.matchAll(TW_EASE)) {
|
|
377
|
+
if (m[1].startsWith("var("))
|
|
378
|
+
continue;
|
|
379
|
+
push("motion", m[1], m.index ?? -1);
|
|
380
|
+
}
|
|
381
|
+
// Vocabulary checks exist only when there is a vocabulary: a system that
|
|
382
|
+
// ships no keyframes left nothing to select from, and silence is honest.
|
|
383
|
+
if (table.keyframes.size > 0 && table.slug) {
|
|
384
|
+
const scope = `ds-${table.slug}-`;
|
|
385
|
+
const vocabulary = new Set([...table.keyframes]
|
|
386
|
+
.filter((k) => k.startsWith(scope))
|
|
387
|
+
.map((k) => k.slice(scope.length)));
|
|
388
|
+
for (const m of line.matchAll(ANIMATE_UTIL)) {
|
|
389
|
+
const name = m[1];
|
|
390
|
+
if (name.startsWith("[")) {
|
|
391
|
+
// An arbitrary animation is a whole hand-rolled shorthand in a
|
|
392
|
+
// class - improvisation by definition once a vocabulary exists.
|
|
393
|
+
push("motion", `animate-${name}`, m.index ?? -1);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (ANIMATE_IDIOM.has(name) || vocabulary.has(name))
|
|
397
|
+
continue;
|
|
398
|
+
push("motion", `animate-${name}`, m.index ?? -1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
311
401
|
});
|
|
312
402
|
return {
|
|
313
403
|
file,
|
|
@@ -328,6 +418,7 @@ export function diagnose(files) {
|
|
|
328
418
|
radius: 0,
|
|
329
419
|
spacing: 0,
|
|
330
420
|
font: 0,
|
|
421
|
+
motion: 0,
|
|
331
422
|
};
|
|
332
423
|
for (const f of flat)
|
|
333
424
|
counts[f.kind] += 1;
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -19,6 +19,7 @@ export const EMPTY_TABLE = {
|
|
|
19
19
|
byName: new Map(),
|
|
20
20
|
byValue: new Map(),
|
|
21
21
|
declared: new Set(),
|
|
22
|
+
keyframes: new Set(),
|
|
22
23
|
};
|
|
23
24
|
const hex2 = (n) => Math.max(0, Math.min(255, Math.round(n)))
|
|
24
25
|
.toString(16)
|
|
@@ -91,6 +92,14 @@ function args(body) {
|
|
|
91
92
|
*/
|
|
92
93
|
export function normalizeValue(raw) {
|
|
93
94
|
const v = raw.trim().toLowerCase().replace(/\s+/g, " ");
|
|
95
|
+
// Time lands on milliseconds: `.3s`, `0.3s` and `300ms` are one value, the
|
|
96
|
+
// same way every colour lands on 8-digit hex. Without this, a system that
|
|
97
|
+
// authors `--ds-motion-durations-base: 0.3s` never names an author's `300ms`.
|
|
98
|
+
const time = /^(\d*\.?\d+)(ms|s)$/.exec(v);
|
|
99
|
+
if (time) {
|
|
100
|
+
const ms = Number.parseFloat(time[1]) * (time[2] === "s" ? 1000 : 1);
|
|
101
|
+
return `${ms}ms`;
|
|
102
|
+
}
|
|
94
103
|
const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/.exec(v);
|
|
95
104
|
if (short) {
|
|
96
105
|
const d = (c) => c + c;
|
|
@@ -369,6 +378,7 @@ export function buildTable(input) {
|
|
|
369
378
|
byName,
|
|
370
379
|
byValue,
|
|
371
380
|
declared: parseDeclaredNames(input.css),
|
|
381
|
+
keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
|
|
372
382
|
};
|
|
373
383
|
}
|
|
374
384
|
/**
|
|
@@ -433,6 +443,7 @@ const FAMILY = {
|
|
|
433
443
|
radius: "--ds-radius-",
|
|
434
444
|
spacing: "--ds-spacing-",
|
|
435
445
|
font: "--ds-typography-",
|
|
446
|
+
motion: "--ds-motion-",
|
|
436
447
|
};
|
|
437
448
|
export function tokenFor(table, literal, kind) {
|
|
438
449
|
const hit = table.byValue.get(normalizeValue(literal));
|
package/dist/guide.js
CHANGED
|
@@ -147,10 +147,18 @@ ${depLines.join("\n")}
|
|
|
147
147
|
loop: "ambient loop · infinite - decorative, use sparingly",
|
|
148
148
|
attention: "attention · one shot, only when asked",
|
|
149
149
|
};
|
|
150
|
+
// Promise only what the ARTIFACTS actually contain, not what this CLI
|
|
151
|
+
// version knows how to describe. First field run (soft-test, 29/07): a
|
|
152
|
+
// newer CLI wrote "already compiled as ready utilities" against a theme.css
|
|
153
|
+
// an older server had compiled without --animate-* keys - the agent
|
|
154
|
+
// verified `animate-rise` emitted nothing and filed the GUIDE's own promise
|
|
155
|
+
// as a gap. The artifacts travel in this payload; reading them is free.
|
|
156
|
+
const hasAnimateKeys = /--animate-/.test(payload.artifacts["theme.css"] ?? "");
|
|
157
|
+
const hasReducedMotionFloor = Object.values(payload.artifacts).some((css) => /prefers-reduced-motion/.test(css ?? ""));
|
|
150
158
|
const keyframeLines = Object.entries(motion.keyframes).map(([kname, frames]) => {
|
|
151
159
|
const kf = frames;
|
|
152
160
|
const intent = classifyKeyframe(kname, kf);
|
|
153
|
-
const usage = hasTailwind
|
|
161
|
+
const usage = hasTailwind && hasAnimateKeys
|
|
154
162
|
? `\`animate-${kebab(kname)}\``
|
|
155
163
|
: `\`animation: ${animationShorthand(`ds-${slug}-${kebab(kname)}`, intent, isFullRotation(kf))}\``;
|
|
156
164
|
return `- ${usage} - ${INTENT_LINE[intent]} (${describeKeyframe(kf)})`;
|
|
@@ -398,7 +406,7 @@ ${keyframeLines.length > 0
|
|
|
398
406
|
|
|
399
407
|
The base is QUIET: nothing moves until someone asks. When they do, animation is **selection
|
|
400
408
|
from this list**, not improvisation - these are the system's own animations, already compiled
|
|
401
|
-
into ${hasTailwind ? "`theme.css` as ready utilities" : `\`tokens.css\` as \`@keyframes ds-${slug}-<name>\``}, each on the clock its intent calls for:
|
|
409
|
+
into ${hasTailwind && hasAnimateKeys ? "`theme.css` as ready utilities" : `\`tokens.css\` as \`@keyframes ds-${slug}-<name>\``}, each on the clock its intent calls for:
|
|
402
410
|
|
|
403
411
|
${keyframeLines.join("\n")}
|
|
404
412
|
${patternLines.length > 0
|
|
@@ -409,8 +417,9 @@ ${patternLines.join("\n")}
|
|
|
409
417
|
`
|
|
410
418
|
: ""}
|
|
411
419
|
Rules: entrances run once, never on scroll-loop; ambient loops are decoration, one per view is
|
|
412
|
-
plenty;
|
|
413
|
-
|
|
420
|
+
plenty; ${hasReducedMotionFloor
|
|
421
|
+
? "`prefers-reduced-motion` is already honored in the shipped CSS (a floor collapses all\nanimations) - don't undo it"
|
|
422
|
+
: "honor `prefers-reduced-motion` yourself (wrap animations in `motion-safe:` or a media query) - this build of the CSS ships no floor"}. If a request needs a motion this vocabulary lacks, do NOT hand-roll
|
|
414
423
|
\`@keyframes\` or raw durations: file it (\`request_token\` via MCP, or \`synthesisui request\`) and
|
|
415
424
|
tell the person what you chose from the vocabulary instead.`
|
|
416
425
|
: ""}
|
package/package.json
CHANGED