synthesisui 0.16.116 → 0.16.118

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.
@@ -13,7 +13,7 @@ import { describeGate, gateComponent, groupSkips, SCREENS_FOR_SYSTEM, } from "..
13
13
  import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
14
14
  import { countShape, describeCoverage, summarizeCoverage, } from "../doctor/coverage.js";
15
15
  import { crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } from "../doctor/crosswalk.js";
16
- import { moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
16
+ import { keyframeOffsets, moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
17
17
  import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
18
18
  import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
19
19
  import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
@@ -22,9 +22,11 @@ import { diagnose, scanSource } from "../doctor/scan.js";
22
22
  import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
23
23
  import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
24
24
  import { sketchOf } from "../doctor/sketch.js";
25
+ import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
25
26
  import { buildTable } from "../doctor/tokens.js";
26
27
  import { readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
27
28
  import { transcribeVariants } from "../doctor/variant-read.js";
29
+ import { frontierKind } from "../frontier-kind.js";
28
30
  import { withLibraryStructure } from "../library-structure.js";
29
31
  import { body, paint, section } from "../output.js";
30
32
  import { phase, startProgress } from "../progress.js";
@@ -306,7 +308,20 @@ export async function takeCensus(root, opts) {
306
308
  /** How much came out of CSS Modules, for the coverage report. */
307
309
  let moduleFiles = 0;
308
310
  let moduleDeclarations = 0;
309
- const moduleKeyframes = {};
311
+ /**
312
+ * THE KEYFRAMES THE READING NEEDS, by name - and this was collected and dropped.
313
+ *
314
+ * Nothing read `moduleKeyframes`: an app whose animation lives in a CSS module lost it
315
+ * in silence, and the four Tailwind ships had nowhere to travel either, so
316
+ * `animation: var(--animate-spin)` would have pointed at a keyframe the system does not
317
+ * define (dono, 04/08). The document's shape is offset → declarations, which is what
318
+ * `keyframeOffsets` turns their raw body into and what `BUILTIN_KEYFRAMES` already is.
319
+ */
320
+ const keyframes = {};
321
+ /** Names their `animate-*` utilities point at - see `Transcription.animations`. */
322
+ const animations = new Set();
323
+ /** Per component, the rules a `<style>` island aims at somebody else's markup. */
324
+ const islands = new Map();
310
325
  /** How each component expresses style, so the census can state its own coverage. */
311
326
  const shapes = new Map();
312
327
  /** Component FILES the gate accepted - the honest denominator for coverage. A first
@@ -484,8 +499,9 @@ export async function takeCensus(root, opts) {
484
499
  moduleFiles += 1;
485
500
  moduleDeclarations += readModule.read;
486
501
  for (const [name, frames] of Object.entries(readModule.keyframes)) {
487
- if (!moduleKeyframes[name])
488
- moduleKeyframes[name] = frames;
502
+ const offsets = keyframeOffsets(frames);
503
+ if (!keyframes[name] && Object.keys(offsets).length > 0)
504
+ keyframes[name] = offsets;
489
505
  }
490
506
  // One module per component is the shape every one of the 530 follows.
491
507
  break;
@@ -527,6 +543,83 @@ export async function takeCensus(root, opts) {
527
543
  * the owner's rule, adopted verbatim (dono, 01/08).
528
544
  */
529
545
  const sketch = sketchOf(src, found[0].name);
546
+ /**
547
+ * THE CSS THE COMPONENT CARRIES INSIDE ITSELF.
548
+ *
549
+ * Their `TypingIndicator` styles with a `<style>` element and nothing else: read as
550
+ * utilities it arrived with three declarations and no animation, so the component
551
+ * that IS an animation previewed as three static circles (dono, 04/08). Three kinds
552
+ * of content, three destinations - see `readStyleIslands`.
553
+ */
554
+ const writes = new Set();
555
+ for (const node of sketch)
556
+ for (const cls of (node.classes ?? "").split(/\s+/))
557
+ if (cls)
558
+ writes.add(cls);
559
+ const island = readStyleIslands(src, writes);
560
+ /** Sketch index → what the island declares for that element. */
561
+ const islandStyle = {};
562
+ for (const [name, frames] of Object.entries(island.keyframes)) {
563
+ keyframes[name] ??= frames;
564
+ animations.add(name);
565
+ }
566
+ /**
567
+ * A PATCH OVER SOMEBODY ELSE'S MARKUP IS A LAW, never a part: giving
568
+ * `.sketch-picker input` a part of ours would claim we drew a colour wheel.
569
+ */
570
+ /**
571
+ * A RULE ON A CLASS THEY WRITE IS THE LOOK OF THE PART THAT WEARS IT.
572
+ *
573
+ * Their three dots are three sketch nodes all carrying `class="dot"`, and the
574
+ * anatomy already named them `dot`, `dot-2`, `dot-3` - three parts with EMPTY
575
+ * bases, because a utility reader has nothing to read on them. The island's `.dot`
576
+ * rule is their shared look and `:nth-child(1..3)` is the delay that makes the
577
+ * wave, one per part, in the order the nodes appear.
578
+ *
579
+ * Carried on the look and merged when the anatomy resolves, which is the only
580
+ * moment a class maps to a part name - the same seam the per-part layers use.
581
+ */
582
+ if (island.own.length > 0) {
583
+ const seen = new Map();
584
+ for (const [index, node] of sketch.entries()) {
585
+ for (const cls of (node.classes ?? "").split(/\s+/)) {
586
+ if (!cls)
587
+ continue;
588
+ const nth = (seen.get(cls) ?? 0) + 1;
589
+ seen.set(cls, nth);
590
+ const mine = island.own.filter((rule) => rule.class === cls && (rule.nth ?? nth) === nth);
591
+ if (mine.length === 0)
592
+ continue;
593
+ islandStyle[index] = Object.assign(islandStyle[index] ?? {}, ...mine.map((rule) => rule.declarations));
594
+ }
595
+ }
596
+ }
597
+ if (island.foreign.length > 0) {
598
+ /**
599
+ * WHOSE MARKUP IS BEING PATCHED, measured rather than guessed: the opaque
600
+ * package this component composes. A headless one renders no markup of its own
601
+ * to patch.
602
+ */
603
+ const drawn = sketch.find((node) => node.from && frontierKind(node.from) === "opaque")?.from;
604
+ islands.set(found[0].name, { foreign: island.foreign, drawn });
605
+ }
606
+ /**
607
+ * THE ANIMATIONS THIS COMPONENT NAMES, from the whole sketch rather than from the
608
+ * root's transcription - an `animate-ping` lives on a part three levels down and
609
+ * the resolver that reads a part does not report back up.
610
+ *
611
+ * The NAME only. The platform owns a library of frames and gives each one this
612
+ * system's own clock; a name it cannot define becomes a dangling reference the
613
+ * screen already knows how to say out loud, which beats a silent `animation` that
614
+ * animates nothing.
615
+ */
616
+ for (const node of sketch) {
617
+ for (const raw of (node.classes ?? "").split(/\s+/)) {
618
+ const named = /^(?:[\w-]+:)*animate-([a-z][\w-]*)$/.exec(raw);
619
+ if (named && named[1] !== "none")
620
+ animations.add(named[1]);
621
+ }
622
+ }
530
623
  /**
531
624
  * A PER-PART LAYER FINDS ITS NODE, by the class string of the call site that
532
625
  * consumes the field. See `ReadLayer.onClasses`: the reader knows a
@@ -567,6 +660,7 @@ export async function takeCensus(root, opts) {
567
660
  ...(tag ? { rootTag: tag } : {}),
568
661
  ...rootPackage(tag, sketch),
569
662
  ...(placed.length > 0 ? { layers: placed } : {}),
663
+ ...(Object.keys(islandStyle).length > 0 ? { islandStyle } : {}),
570
664
  ...(v.unslotted.length > 0
571
665
  ? { unplaced: [...new Set(v.unslotted)] }
572
666
  : {}),
@@ -1014,6 +1108,19 @@ export async function takeCensus(root, opts) {
1014
1108
  * happens, which is the whole reason `--usage` exists.
1015
1109
  */
1016
1110
  const laws = new Map();
1111
+ /**
1112
+ * A PATCH OVER SOMEBODY ELSE'S MARKUP IS A LAW, and it is written here because this is
1113
+ * where laws are collected - giving `.sketch-picker input` a part of ours would claim we
1114
+ * drew a colour wheel. See `readStyleIslands`.
1115
+ */
1116
+ for (const [name, read] of islands) {
1117
+ const line = describeForeign(read.foreign, read.drawn);
1118
+ if (!line)
1119
+ continue;
1120
+ const own = laws.get(name) ?? [];
1121
+ own.push(line);
1122
+ laws.set(name, own);
1123
+ }
1017
1124
  for (const r of observedRules(merged)) {
1018
1125
  laws.set(r.component, [...(laws.get(r.component) ?? []), r.text]);
1019
1126
  }
@@ -1127,6 +1234,8 @@ export async function takeCensus(root, opts) {
1127
1234
  census: 1,
1128
1235
  project: { name, stack: await detectStack(root) },
1129
1236
  declared: Object.fromEntries(table.byName),
1237
+ ...(Object.keys(keyframes).length > 0 ? { keyframes } : {}),
1238
+ ...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
1130
1239
  ...(brokenRefs.length > 0 ? { brokenRefs } : {}),
1131
1240
  ...(conventions.length > 0 ? { conventions } : {}),
1132
1241
  classStyle,
@@ -1903,6 +2012,26 @@ async function resolveReadParts(census, root) {
1903
2012
  const split = placeLayers((look?.layers ?? []), partAt, (look?.sketch ?? []));
1904
2013
  for (const line of split.unplaced)
1905
2014
  notes.add(line);
2015
+ /**
2016
+ * WHAT A `<style>` ISLAND DECLARED, ONTO THE PART THAT WEARS IT.
2017
+ *
2018
+ * Their three dots share `class="dot"` and the anatomy named them `dot`, `dot-2`,
2019
+ * `dot-3` - three parts with empty bases, because a utility reader has nothing to read
2020
+ * on a component that styles itself in CSS. This is the first moment an index maps to a
2021
+ * part name, which is why it happens here and not where the island was read.
2022
+ *
2023
+ * A utility of theirs on the same element still wins: the island only fills what the
2024
+ * class string did not already say.
2025
+ */
2026
+ for (const [index, style] of Object.entries(look?.islandStyle ?? {})) {
2027
+ const part = partAt[Number(index)];
2028
+ const target = part ? resolved.parts[part] : undefined;
2029
+ if (!target) {
2030
+ notes.add("a `<style>` element declares a look for an element the anatomy does not name - it is not applied, because that would paint the wrong thing");
2031
+ continue;
2032
+ }
2033
+ target.base = { ...style, ...target.base };
2034
+ }
1906
2035
  /**
1907
2036
  * REPLACED, NOT APPENDED - because this runs more than once.
1908
2037
  *
@@ -74,7 +74,7 @@ export function partName(authored) {
74
74
  .replace(/^-|-$/g, "");
75
75
  }
76
76
  /** Comments out, so a `{` inside one cannot end a block early. */
77
- function stripCssComments(css) {
77
+ export function stripCssComments(css) {
78
78
  return css.replace(/\/\*[\s\S]*?\*\//g, "");
79
79
  }
80
80
  function rules(css, media = null, out = []) {
@@ -168,6 +168,40 @@ function emptyClass() {
168
168
  * same way the utility reader does - so the value travels as `{radius.md}` rather than
169
169
  * as a literal, and the recipe re-themes.
170
170
  */
171
+ /**
172
+ * A KEYFRAME'S OWN OFFSETS, so the document can hold it.
173
+ *
174
+ * `keyframes` carried the body as raw CSS text and it was collected and then dropped -
175
+ * `moduleKeyframes` in `import.ts` had no reader at all, so an app whose animation lives
176
+ * in a CSS module lost it in silence (dono, 04/08). The document's shape is offset →
177
+ * declarations, which is the same rule parser one level down.
178
+ */
179
+ export function keyframeOffsets(body) {
180
+ const out = {};
181
+ for (const rule of rules(stripCssComments(body))) {
182
+ /**
183
+ * `0%,\n 100%` is one offset written across two lines. Kept verbatim it becomes a
184
+ * selector with a newline in it, which the compiler emits and the browser ignores.
185
+ */
186
+ const offset = rule.selector.replace(/\s+/g, " ").trim();
187
+ if (!offset)
188
+ continue;
189
+ const block = {};
190
+ for (const line of rule.body.split(";")) {
191
+ const at = line.indexOf(":");
192
+ if (at === -1)
193
+ continue;
194
+ const prop = line.slice(0, at).trim();
195
+ const value = line.slice(at + 1).trim();
196
+ if (!prop || !value)
197
+ continue;
198
+ block[prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
199
+ }
200
+ if (Object.keys(block).length > 0)
201
+ out[offset] = block;
202
+ }
203
+ return out;
204
+ }
171
205
  export function readModuleCss(css) {
172
206
  const out = { classes: {}, keyframes: {}, unslotted: [] };
173
207
  const clean = stripCssComments(css);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * THE CSS A COMPONENT CARRIES INSIDE ITSELF.
3
+ *
4
+ * Some components do not style with utilities at all - they render a `<style>` element
5
+ * with real CSS in it. Their `TypingIndicator` is the whole case: three dots, one
6
+ * `@keyframes wave-bounce`, and a `.dot` rule with a per-dot delay. Read as utilities it
7
+ * arrived with three declarations and no animation, so the component that IS an animation
8
+ * previewed as three static circles (dono, 04/08).
9
+ *
10
+ * Measured across their repository: 2 islands, both in `packages/ui`, and none in any of
11
+ * the three apps - 1 keyframe, 6 rules on classes the component itself writes, 3 on a
12
+ * third party's internals, and 12 `!important`.
13
+ *
14
+ * THREE KINDS OF CONTENT, THREE DESTINATIONS, and telling them apart is the whole job:
15
+ *
16
+ * @keyframes the system's motion vocabulary, by name
17
+ * a rule on a class THEY write the look of the part that wears that class
18
+ * a rule on somebody else's a patch over a third party: prose, never a look
19
+ *
20
+ * The last one is the line. `.sketch-picker input { background: … !important }` is not
21
+ * their component's anatomy - it is a re-skin of `react-color`'s own markup - and giving
22
+ * it a part would claim we drew something we did not. It travels as a law instead, with
23
+ * the package named and the `!important` said out loud.
24
+ */
25
+ import { keyframeOffsets, readModuleCss, stripCssComments, } from "./css-modules.js";
26
+ /** `<style>{`…`}</style>` and `<style>…</style>` - the two spellings in JSX. */
27
+ const ISLAND = /<style[^>]*>\s*\{?\s*(?:`([\s\S]*?)`|([\s\S]*?))\s*\}?\s*<\/style>/g;
28
+ /** `.dot:nth-child(2)` → the class and which one. A compound selector is foreign. */
29
+ const OWN_SELECTOR = /^\.([\w-]+)(?::nth-child\((\d+)\))?$/;
30
+ /**
31
+ * The CSS a component keeps inside itself, split by where each rule belongs.
32
+ *
33
+ * `writes` is the set of classes the component's own markup carries - from the sketch,
34
+ * which measured them. A selector naming one of those is about an element we know; any
35
+ * other selector is about markup somebody else renders.
36
+ */
37
+ export function readStyleIslands(source, writes) {
38
+ const out = { keyframes: {}, own: [], foreign: [] };
39
+ ISLAND.lastIndex = 0;
40
+ for (const island of source.matchAll(ISLAND)) {
41
+ const raw = island[1] ?? island[2] ?? "";
42
+ if (!raw.trim())
43
+ continue;
44
+ /**
45
+ * COMMENTS OUT FIRST. Their `.dot` rule carries `/* Cor ocean-600 *``/` between two
46
+ * declarations, and the split-on-semicolon read it as part of the NEXT property's
47
+ * name - so `borderRadius` arrived as `/* Cor … *``/\n borderRadius`.
48
+ */
49
+ const body = stripCssComments(raw);
50
+ const read = readModuleCss(body);
51
+ for (const [name, frames] of Object.entries(read.keyframes)) {
52
+ const offsets = keyframeOffsets(frames);
53
+ if (Object.keys(offsets).length > 0)
54
+ out.keyframes[name] ??= offsets;
55
+ }
56
+ /**
57
+ * `readModuleCss` files a rule under each class it names, which is what a CSS module
58
+ * needs - a class there IS the export. Here the SELECTOR decides, because
59
+ * `.dark .sketch-picker span` names a class we write (`dark`) and is still about
60
+ * somebody else's span. So the rules are re-read off the body.
61
+ */
62
+ for (const rule of body.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
63
+ const selector = rule[1].trim().split("\n").pop()?.trim() ?? "";
64
+ if (!selector || selector.startsWith("@"))
65
+ continue;
66
+ // An offset inside a keyframe body is not a rule about an element.
67
+ if (/^(?:from|to|\d+%)(?:\s*,\s*(?:from|to|\d+%))*$/.test(selector))
68
+ continue;
69
+ const declarations = {};
70
+ let important = 0;
71
+ for (const line of rule[2].split(";")) {
72
+ const at = line.indexOf(":");
73
+ if (at === -1)
74
+ continue;
75
+ const property = line.slice(0, at).trim();
76
+ let value = line.slice(at + 1).trim();
77
+ if (!property || !value)
78
+ continue;
79
+ if (/!important$/.test(value)) {
80
+ important += 1;
81
+ value = value.replace(/\s*!important$/, "");
82
+ }
83
+ declarations[property.replace(/-([a-z])/g, (_m, c) => c.toUpperCase())] = value;
84
+ }
85
+ if (Object.keys(declarations).length === 0)
86
+ continue;
87
+ /**
88
+ * THEIR `animation: wave-bounce 1s infinite` NAMES A KEYFRAME THE SYSTEM OWNS.
89
+ *
90
+ * The compiler scopes every keyframe and mints `--animate-<name>` for it, so a raw
91
+ * name in a recipe points at nothing. The reference replaces the name and their own
92
+ * timing stays as longhands beside it - which is what makes the wave a wave.
93
+ */
94
+ if (declarations.animation) {
95
+ const named = /^([\w-]+)\s*(.*)$/.exec(declarations.animation.trim());
96
+ if (named && out.keyframes[named[1]]) {
97
+ declarations.animation = `var(--animate-${named[1]})`;
98
+ for (const part of named[2].split(/\s+/).filter(Boolean)) {
99
+ if (/^\d+(?:\.\d+)?m?s$/.test(part))
100
+ declarations.animationDuration ??= part;
101
+ else if (part === "infinite" || /^\d+$/.test(part))
102
+ declarations.animationIterationCount ??= part;
103
+ else if (/^(linear|ease|ease-in|ease-out|ease-in-out|steps\(|cubic-bezier\()/.test(part))
104
+ declarations.animationTimingFunction ??= part;
105
+ }
106
+ }
107
+ }
108
+ const own = OWN_SELECTOR.exec(selector);
109
+ if (own && writes.has(own[1])) {
110
+ out.own.push({
111
+ class: own[1],
112
+ ...(own[2] ? { nth: Number(own[2]) } : {}),
113
+ declarations,
114
+ });
115
+ continue;
116
+ }
117
+ out.foreign.push({
118
+ selector,
119
+ declarations: Object.keys(declarations).length,
120
+ important,
121
+ });
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+ /**
127
+ * What a patch over somebody else's component is, said in one line.
128
+ *
129
+ * A law rather than a look: an agent reading this system needs to know the re-skin exists
130
+ * and that it leans on `!important`, and no part of ours should claim to have drawn it.
131
+ */
132
+ export function describeForeign(rules, library) {
133
+ if (rules.length === 0)
134
+ return null;
135
+ const declarations = rules.reduce((n, r) => n + r.declarations, 0);
136
+ const important = rules.reduce((n, r) => n + r.important, 0);
137
+ const selectors = [...new Set(rules.map((r) => r.selector))].slice(0, 3);
138
+ return (`This component re-skins ${library ?? "a third party's markup"} from the inside: ` +
139
+ `${declarations} declaration${declarations === 1 ? "" : "s"} across ${rules.length} rule${rules.length === 1 ? "" : "s"} ` +
140
+ `in a \`<style>\` element it renders itself, on selectors that are not ours ` +
141
+ `(${selectors.join(", ")})${important > 0 ? `, ${important} of them \`!important\`` : ""}. ` +
142
+ `Those are not part styles and editing this system's tokens will not move them.`);
143
+ }
@@ -449,6 +449,36 @@ const PLACE_KEYWORD = {
449
449
  value: "overflow:hidden|textOverflow:ellipsis|whiteSpace:nowrap",
450
450
  },
451
451
  };
452
+ /**
453
+ * MOVEMENT IS A DECLARATION TOO, and 91 utilities of it were being dropped.
454
+ *
455
+ * Measured across their 36 components (dono, 04/08): 34 `transition-*` in 20 components,
456
+ * 23 `duration-*` in 16, 2 `ease-*`, 18 `animate-*` in 10. Their chevron turns over 300ms
457
+ * and their spinner spins - and the recipe said neither, so a rebuilt component snapped
458
+ * between states and sat still where it should turn.
459
+ *
460
+ * READ AS THE FOUR LONGHANDS, never as the shorthand. `transition-colors duration-200
461
+ * ease-out` is three utilities and three properties: composing them into one `transition`
462
+ * would mean guessing what the other two will be, and whichever landed last would erase
463
+ * the others. Longhands compose the way their own utilities do.
464
+ */
465
+ const TRANSITION_PROPERTY = {
466
+ all: "all",
467
+ none: "none",
468
+ colors: "color, background-color, border-color, text-decoration-color, fill, stroke",
469
+ opacity: "opacity",
470
+ shadow: "box-shadow",
471
+ transform: "transform, translate, scale, rotate",
472
+ // Tailwind's bare `transition` is this set, and it is what 16 of theirs write.
473
+ "": "color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter",
474
+ };
475
+ /** Tailwind's easing scale, verbatim - a curve nobody should retype. */
476
+ const EASING = {
477
+ linear: "linear",
478
+ in: "cubic-bezier(0.4, 0, 1, 1)",
479
+ out: "cubic-bezier(0, 0, 0.2, 1)",
480
+ "in-out": "cubic-bezier(0.4, 0, 0.2, 1)",
481
+ };
452
482
  /** `justify-between` → `space-between`, and the four that are not a straight copy. */
453
483
  const ALIGNMENT = {
454
484
  start: "flex-start",
@@ -616,6 +646,49 @@ function readUtilityCore(utility, declared) {
616
646
  }
617
647
  : { property: "borderRadius", value: "0.25rem" };
618
648
  }
649
+ /**
650
+ * TRANSITION, DURATION, EASING, DELAY - four utilities, four properties.
651
+ *
652
+ * 59 of these across their library, and the recipe carried none: a rebuilt component
653
+ * snapped between every state their own code eases through (dono, 04/08).
654
+ */
655
+ const bareTransition = /^transition(?:-(.+))?$/.exec(core);
656
+ if (bareTransition) {
657
+ const arg = bareTransition[1] ?? "";
658
+ const arbitrary = /^\[(.+)\]$/.exec(arg);
659
+ const value = arbitrary
660
+ ? arbitrary[1].replace(/_/g, " ")
661
+ : TRANSITION_PROPERTY[arg];
662
+ if (value)
663
+ return { property: "transitionProperty", value };
664
+ }
665
+ const ms = /^(duration|delay)-(\d+)$/.exec(core);
666
+ if (ms) {
667
+ return {
668
+ property: ms[1] === "duration" ? "transitionDuration" : "transitionDelay",
669
+ value: `${ms[2]}ms`,
670
+ };
671
+ }
672
+ const ease = /^ease-(.+)$/.exec(core);
673
+ if (ease && EASING[ease[1]]) {
674
+ return { property: "transitionTimingFunction", value: EASING[ease[1]] };
675
+ }
676
+ /**
677
+ * `animate-spin` - the animation, by the name the system defines it under.
678
+ *
679
+ * `var(--animate-spin)` rather than a shorthand of ours: the compiler mints that
680
+ * variable from the document's own keyframes and gives it this system's clock. The
681
+ * frames themselves travel beside the reading - see `BUILTIN_KEYFRAMES` - because a
682
+ * recipe cannot point at a keyframe the system does not define.
683
+ */
684
+ const animate = /^animate-([a-z][\w-]*)$/.exec(core);
685
+ if (animate && animate[1] !== "none") {
686
+ return {
687
+ property: "animation",
688
+ value: `var(--animate-${animate[1]})`,
689
+ keyframe: animate[1],
690
+ };
691
+ }
619
692
  /**
620
693
  * ALIGNMENT - `justify-between`, `items-center`, `self-end`.
621
694
  *
@@ -1552,6 +1625,11 @@ export function transcribe(classes, declared) {
1552
1625
  out.fromLiteral += 1;
1553
1626
  continue;
1554
1627
  }
1628
+ if (decl.keyframe) {
1629
+ out.animations ??= [];
1630
+ if (!out.animations.includes(decl.keyframe))
1631
+ out.animations.push(decl.keyframe);
1632
+ }
1555
1633
  if (target[decl.property] != null)
1556
1634
  continue;
1557
1635
  target[decl.property] = decl.value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.116",
3
+ "version": "0.16.118",
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": {