synthesisui 0.16.26 → 0.16.27

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.
@@ -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. */
@@ -59,6 +59,28 @@ 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
+ const MOTION_PROP = /\b(?:transition|animation)(?:-(?:duration|delay|timing-function)|Duration|Delay|TimingFunction)?\s*:\s*["'`]?([^;}"'`\n]*)/g;
73
+ const TIME_LITERAL = /(\d*\.?\d+)(ms|s)\b/g;
74
+ const BEZIER = /cubic-bezier\([^)]*\)/g;
75
+ /** `duration-300`, `duration-[350ms]`, `delay-75` - Tailwind's stock clock. */
76
+ const TW_TIME = /\b(?:duration|delay)-(?:\[([^\]]+)\]|(\d+)\b)/g;
77
+ const TW_EASE = /\bease-\[([^\]]+)\]/g;
78
+ /** `animate-fade-up`, `animate-[wiggle_1s_ease]` - checked against the
79
+ * installed vocabulary, so only when the system ships one. */
80
+ const ANIMATE_UTIL = /\banimate-(\[[^\]]+\]|[a-z0-9-]+)/g;
81
+ /** tailwindcss-animate's grammar (the shadcn bridge): `animate-in`/`-out` are
82
+ * composable micro-transition idiom, not a keyframe selection to police. */
83
+ const ANIMATE_IDIOM = new Set(["none", "in", "out"]);
62
84
  /**
63
85
  * Uses of the system. Coverage is meaningless without them.
64
86
  *
@@ -308,6 +330,61 @@ export function scanSource(file, source, table) {
308
330
  continue;
309
331
  push("font", stack);
310
332
  }
333
+ for (const m of line.matchAll(MOTION_PROP)) {
334
+ const value = m[1];
335
+ const base = (m.index ?? 0) + m[0].length - value.length;
336
+ for (const t of value.matchAll(TIME_LITERAL)) {
337
+ // `0s` is idiom (disabling a transition), not a clock chosen by hand.
338
+ if (Number.parseFloat(t[1]) === 0)
339
+ continue;
340
+ push("motion", `${t[1]}${t[2]}`, base + (t.index ?? 0));
341
+ }
342
+ for (const b of value.matchAll(BEZIER)) {
343
+ push("motion", b[0], base + (b.index ?? 0));
344
+ }
345
+ // A `ds-*` animation name that the installed css never declares renders
346
+ // as NOTHING, silently - the keyframe cousin of a phantom token.
347
+ if (table.keyframes.size > 0) {
348
+ for (const w of value.matchAll(/\b(ds-[a-z0-9-]+)\b/g)) {
349
+ if (!table.keyframes.has(w[1]))
350
+ phantoms.push({ name: `@keyframes ${w[1]}`, line: at });
351
+ }
352
+ }
353
+ }
354
+ for (const m of line.matchAll(TW_TIME)) {
355
+ const literal = m[1] ?? `${m[2]}ms`;
356
+ // `duration-[var(--ds-motion-durations-base)]` is the token being USED.
357
+ if (literal.startsWith("var("))
358
+ continue;
359
+ if (Number.parseFloat(literal) === 0)
360
+ continue;
361
+ push("motion", literal, m.index ?? -1);
362
+ }
363
+ for (const m of line.matchAll(TW_EASE)) {
364
+ if (m[1].startsWith("var("))
365
+ continue;
366
+ push("motion", m[1], m.index ?? -1);
367
+ }
368
+ // Vocabulary checks exist only when there is a vocabulary: a system that
369
+ // ships no keyframes left nothing to select from, and silence is honest.
370
+ if (table.keyframes.size > 0 && table.slug) {
371
+ const scope = `ds-${table.slug}-`;
372
+ const vocabulary = new Set([...table.keyframes]
373
+ .filter((k) => k.startsWith(scope))
374
+ .map((k) => k.slice(scope.length)));
375
+ for (const m of line.matchAll(ANIMATE_UTIL)) {
376
+ const name = m[1];
377
+ if (name.startsWith("[")) {
378
+ // An arbitrary animation is a whole hand-rolled shorthand in a
379
+ // class - improvisation by definition once a vocabulary exists.
380
+ push("motion", `animate-${name}`, m.index ?? -1);
381
+ continue;
382
+ }
383
+ if (ANIMATE_IDIOM.has(name) || vocabulary.has(name))
384
+ continue;
385
+ push("motion", `animate-${name}`, m.index ?? -1);
386
+ }
387
+ }
311
388
  });
312
389
  return {
313
390
  file,
@@ -328,6 +405,7 @@ export function diagnose(files) {
328
405
  radius: 0,
329
406
  spacing: 0,
330
407
  font: 0,
408
+ motion: 0,
331
409
  };
332
410
  for (const f of flat)
333
411
  counts[f.kind] += 1;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.26",
3
+ "version": "0.16.27",
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": {