kuinetic 0.1.3 → 0.2.0

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.
Files changed (125) hide show
  1. package/README.md +93 -10
  2. package/dist/esm/advanced/index.mjs +4727 -0
  3. package/dist/esm/chunk-AEQGC2KM.mjs +189 -0
  4. package/dist/esm/chunk-MQSNMYRP.mjs +9332 -0
  5. package/dist/esm/chunk-RVTYN6NZ.mjs +3446 -0
  6. package/dist/esm/chunk-U43BX5O2.mjs +2984 -0
  7. package/dist/esm/core/index.mjs +16 -9
  8. package/dist/esm/effects/index.mjs +19 -4
  9. package/dist/esm/index.mjs +17 -10
  10. package/dist/kuinetic.advanced.js +5070 -0
  11. package/dist/kuinetic.advanced.min.js +749 -0
  12. package/dist/kuinetic.all.js +15218 -5595
  13. package/dist/kuinetic.all.min.js +2 -0
  14. package/dist/kuinetic.css +2900 -360
  15. package/dist/kuinetic.js +15217 -5593
  16. package/dist/kuinetic.min.css +1 -0
  17. package/dist/kuinetic.min.js +7 -0
  18. package/dist/types/3d/angles.d.ts +30 -0
  19. package/dist/types/3d/flattening.d.ts +49 -0
  20. package/dist/types/3d/index.d.ts +36 -0
  21. package/dist/types/3d/model-3d.d.ts +115 -0
  22. package/dist/types/3d/register.d.ts +26 -0
  23. package/dist/types/3d/webgl.d.ts +24 -0
  24. package/dist/types/advanced/audio.d.ts +322 -0
  25. package/dist/types/advanced/base.d.ts +233 -0
  26. package/dist/types/advanced/camera-3d.d.ts +236 -0
  27. package/dist/types/advanced/fluid-cursor.d.ts +149 -0
  28. package/dist/types/advanced/gl-utils.d.ts +52 -0
  29. package/dist/types/advanced/glsl.d.ts +60 -0
  30. package/dist/types/advanced/index.d.ts +23 -0
  31. package/dist/types/advanced/particles.d.ts +108 -0
  32. package/dist/types/advanced/scenes.d.ts +207 -0
  33. package/dist/types/advanced/shaders.d.ts +972 -0
  34. package/dist/types/browser/boot.d.ts +72 -0
  35. package/dist/types/core/activation.d.ts +227 -2
  36. package/dist/types/core/animator.d.ts +446 -11
  37. package/dist/types/core/attrs.d.ts +9 -0
  38. package/dist/types/core/breakpoints.d.ts +260 -0
  39. package/dist/types/core/bundles.d.ts +71 -0
  40. package/dist/types/core/callback.d.ts +64 -0
  41. package/dist/types/core/capabilities.d.ts +47 -0
  42. package/dist/types/core/channels.d.ts +120 -2
  43. package/dist/types/core/cloak-selectors.d.ts +47 -0
  44. package/dist/types/core/compile.d.ts +207 -2
  45. package/dist/types/core/control.d.ts +218 -0
  46. package/dist/types/core/declarations.d.ts +128 -0
  47. package/dist/types/core/derived/aggregate.d.ts +43 -0
  48. package/dist/types/core/derived/book.d.ts +30 -0
  49. package/dist/types/core/derived/diagnostics.d.ts +51 -0
  50. package/dist/types/core/derived/group-gate.d.ts +62 -0
  51. package/dist/types/core/derived/install.d.ts +110 -0
  52. package/dist/types/core/derived/late-matches.d.ts +19 -0
  53. package/dist/types/core/derived/types.d.ts +108 -0
  54. package/dist/types/core/easing.d.ts +57 -0
  55. package/dist/types/core/element-config.d.ts +47 -6
  56. package/dist/types/core/element-size.d.ts +23 -0
  57. package/dist/types/core/event-sources.d.ts +39 -0
  58. package/dist/types/core/flip.d.ts +34 -1
  59. package/dist/types/core/gesture.d.ts +12 -0
  60. package/dist/types/core/host-facts.d.ts +155 -0
  61. package/dist/types/core/index.d.ts +7 -3
  62. package/dist/types/core/owned-styles.d.ts +129 -2
  63. package/dist/types/core/params.d.ts +48 -0
  64. package/dist/types/core/parse.d.ts +28 -1
  65. package/dist/types/core/register-into.d.ts +29 -0
  66. package/dist/types/core/registry.d.ts +10 -3
  67. package/dist/types/core/repeat.d.ts +91 -0
  68. package/dist/types/core/sequence.d.ts +126 -0
  69. package/dist/types/core/stagger-config.d.ts +199 -0
  70. package/dist/types/core/stagger-keys.d.ts +63 -0
  71. package/dist/types/core/stagger.d.ts +187 -6
  72. package/dist/types/core/style-plan.d.ts +21 -16
  73. package/dist/types/core/target.d.ts +128 -0
  74. package/dist/types/core/threshold-reachability.d.ts +45 -0
  75. package/dist/types/core/time-scale.d.ts +9 -0
  76. package/dist/types/core/toggle-actions.d.ts +141 -0
  77. package/dist/types/core/travel.d.ts +59 -0
  78. package/dist/types/core/types.d.ts +705 -13
  79. package/dist/types/core/unquoted-selectors.d.ts +53 -0
  80. package/dist/types/effects/carousel/drag.d.ts +80 -0
  81. package/dist/types/effects/carousel/index.d.ts +149 -0
  82. package/dist/types/effects/catalog/background-media.d.ts +152 -0
  83. package/dist/types/effects/catalog/discrete.d.ts +13 -0
  84. package/dist/types/effects/catalog/interaction-proximity.d.ts +24 -0
  85. package/dist/types/effects/catalog/interaction-reveal.d.ts +248 -0
  86. package/dist/types/effects/catalog/interaction-shared.d.ts +42 -0
  87. package/dist/types/effects/catalog/interaction-states.d.ts +68 -0
  88. package/dist/types/effects/catalog/materials.d.ts +67 -0
  89. package/dist/types/effects/catalog/media-shared.d.ts +3 -2
  90. package/dist/types/effects/catalog/numbers-shared.d.ts +8 -5
  91. package/dist/types/effects/catalog/shared.d.ts +2 -2
  92. package/dist/types/effects/catalog/text-shared.d.ts +92 -10
  93. package/dist/types/effects/catalog/transforms.d.ts +13 -0
  94. package/dist/types/effects/catalog/view-transitions.d.ts +13 -0
  95. package/dist/types/effects/forms/primitives.d.ts +15 -4
  96. package/dist/types/effects/gestures/index.d.ts +53 -0
  97. package/dist/types/effects/index.d.ts +5 -0
  98. package/dist/types/effects/layout/presets.d.ts +35 -0
  99. package/dist/types/effects/motion-path/index.d.ts +35 -0
  100. package/dist/types/effects/scroll-mechanics/params.d.ts +23 -0
  101. package/dist/types/effects/scroll-mechanics/presets.d.ts +51 -0
  102. package/dist/types/effects/scroll-mechanics/section-index.d.ts +47 -0
  103. package/dist/types/effects/shared.d.ts +162 -2
  104. package/dist/types/effects/step-index.d.ts +121 -0
  105. package/dist/types/effects/step-marking.d.ts +41 -33
  106. package/dist/types/effects/svg/icon-parts.d.ts +23 -0
  107. package/dist/types/effects/three-d/flip-parts.d.ts +46 -0
  108. package/dist/types/effects/tween/index.d.ts +57 -0
  109. package/dist/types/effects/tween/properties.d.ts +107 -0
  110. package/dist/types/effects/tween/waypoints.d.ts +106 -0
  111. package/dist/types/showcase/compare.d.ts +4 -0
  112. package/dist/types/showcase/device-frame.d.ts +3 -0
  113. package/dist/types/showcase/hotspots.d.ts +3 -0
  114. package/dist/types/showcase/index.d.ts +30 -0
  115. package/dist/types/showcase/lightbox.d.ts +3 -0
  116. package/dist/types/showcase/media-source.d.ts +13 -0
  117. package/dist/types/showcase/modal-shell.d.ts +22 -0
  118. package/dist/types/showcase/scroll-story.d.ts +4 -0
  119. package/dist/types/showcase/shared.d.ts +47 -0
  120. package/dist/types/showcase/slideshow.d.ts +3 -0
  121. package/dist/types/showcase/slow-mo.d.ts +5 -0
  122. package/package.json +14 -7
  123. package/dist/esm/chunk-5DON3UFQ.mjs +0 -4380
  124. package/dist/esm/chunk-JT4PZL3A.mjs +0 -773
  125. package/dist/esm/chunk-QUVFODSQ.mjs +0 -1278
@@ -0,0 +1,3446 @@
1
+ import {
2
+ ATTR,
3
+ KUI_EVENT,
4
+ Registry,
5
+ SEGMENT_HOIST_KEYS,
6
+ applyToggleVerb,
7
+ assignOnce,
8
+ breakpointsIn,
9
+ control,
10
+ createActivationBinder,
11
+ createCssInstance,
12
+ createGateWatcher,
13
+ createRootResolver,
14
+ createScrollScheduler,
15
+ cssEasingValue,
16
+ directionValue,
17
+ emitLifecycle,
18
+ gateMatches,
19
+ gatedAnimationName,
20
+ gatesOverlap,
21
+ isInfiniteRepeat,
22
+ isOneShot,
23
+ isSafeCssValue,
24
+ parse,
25
+ parseActivationAttribute,
26
+ parseToggleActions,
27
+ play,
28
+ playbackExpression,
29
+ queryScoped,
30
+ readEffectParams,
31
+ readEffectTiming,
32
+ repeatCount,
33
+ resolveActivationSpec,
34
+ resolveParams,
35
+ resolvePlayback,
36
+ selectorBreadth,
37
+ splitTopLevel,
38
+ startKindOf,
39
+ suggest,
40
+ timingProperty,
41
+ toMilliseconds,
42
+ warnAboutActivation,
43
+ warnAboutToggleActions
44
+ } from "./chunk-U43BX5O2.mjs";
45
+ import {
46
+ createLedgerSet
47
+ } from "./chunk-AEQGC2KM.mjs";
48
+
49
+ // src/core/bundles.ts
50
+ var HOIST_KEYS = ["activation", "timeline", "threshold", "rm", "func"];
51
+ var HOIST_LABELS = {
52
+ activation: "on",
53
+ timeline: "timeline",
54
+ threshold: "threshold",
55
+ rm: "rm",
56
+ func: "func"
57
+ };
58
+ var BundleTable = class {
59
+ /**
60
+ * @param registry - Consulted for name collisions only; a bundle never enters it.
61
+ */
62
+ constructor(registry) {
63
+ this.registry = registry;
64
+ }
65
+ bundles = /* @__PURE__ */ new Map();
66
+ /**
67
+ * Register every definition in a subtree, including the subtree root itself.
68
+ *
69
+ * @returns How many *new* names were registered, so the caller can decide whether anything it
70
+ * already compiled deserves a second chance.
71
+ * @complexity O(n) time in the subtree's element count; O(d) space in definition count.
72
+ * @overallScore 100
73
+ */
74
+ collect(root, reporter) {
75
+ let added = 0;
76
+ for (const el of definitionsIn(root)) {
77
+ if (this.define(el, reporter)) added++;
78
+ }
79
+ return added;
80
+ }
81
+ /**
82
+ * Rewrite bundle references into the segments they stand for.
83
+ *
84
+ * @returns A new `ParsedValue`; the argument is returned untouched when nothing is defined.
85
+ * @complexity O(s) time in the total segment count across the bundles reached; O(s) space.
86
+ * @overallScore 100
87
+ */
88
+ expand(parsed) {
89
+ if (this.bundles.size === 0) return parsed;
90
+ const sink = { warnings: [...parsed.warnings], hoists: {} };
91
+ const specs = this.expandList(parsed.specs, [], sink);
92
+ const result = { ...parsed, specs, warnings: sink.warnings };
93
+ for (const key of HOIST_KEYS) absorbHoist(result, sink.hoists, key);
94
+ return result;
95
+ }
96
+ /**
97
+ * Register one definition element.
98
+ *
99
+ * @returns Whether this call added a name the table did not have.
100
+ * @complexity O(n) time in the body's length; O(e) space in its segment count.
101
+ * @overallScore 100
102
+ */
103
+ define(el, reporter) {
104
+ const name = el.getAttribute(ATTR.define).trim();
105
+ const source = el.getAttribute(ATTR.source) ?? "";
106
+ const problem = this.nameProblem(name);
107
+ if (problem) {
108
+ reporter.warn(problem, el);
109
+ return false;
110
+ }
111
+ const existing = this.bundles.get(name);
112
+ if (existing) {
113
+ if (existing.source !== source) {
114
+ reporter.warn(`bundle "${name}" is already defined \u2014 the first definition wins`, el);
115
+ }
116
+ return false;
117
+ }
118
+ if (el.localName !== "template") {
119
+ reporter.warn(
120
+ `bundle "${name}" is defined on <${el.localName}> \u2014 a definition is data, so it is never animated; use <template>`,
121
+ el
122
+ );
123
+ }
124
+ this.bundles.set(name, readBundle(name, source, el, reporter));
125
+ return true;
126
+ }
127
+ /**
128
+ * Refuse a name that cannot work, with the reason.
129
+ *
130
+ * @returns The diagnostic, or `undefined` when the name is usable.
131
+ * @complexity O(n) time in the name's length; O(1) space.
132
+ * @overallScore 100
133
+ */
134
+ nameProblem(name) {
135
+ if (!name) return `${ATTR.define} needs a name \u2014 an empty one defines nothing`;
136
+ if (/[\s,:()"']/.test(name)) {
137
+ return `bundle name "${name}" cannot contain whitespace, a comma, a colon, parentheses or quotes \u2014 those are grammar, so a name carrying them could never be written in ${ATTR.source}`;
138
+ }
139
+ if (this.registry.has(name)) {
140
+ return `bundle "${name}" is already the name of a registered effect \u2014 the effect wins and this definition is ignored`;
141
+ }
142
+ return void 0;
143
+ }
144
+ /**
145
+ * Expand one comma list, recursing through bundles that name other bundles.
146
+ *
147
+ * @param trail - Bundle names currently being expanded, innermost last. Both the cycle guard and
148
+ * its diagnostic read it, so it is a list rather than a set.
149
+ * @complexity O(s) time in the total segment count across the bundles reached; O(s) space.
150
+ * @overallScore 100
151
+ */
152
+ expandList(specs, trail, sink) {
153
+ const out = [];
154
+ for (const spec of specs) {
155
+ const bundle = this.bundles.get(spec.name);
156
+ if (!bundle) {
157
+ out.push(spec);
158
+ this.noteNearMiss(spec.name, sink.warnings);
159
+ continue;
160
+ }
161
+ if (trail.includes(spec.name)) {
162
+ sink.warnings.push(cycleWarning(trail, spec.name));
163
+ continue;
164
+ }
165
+ takeHoists(bundle, spec.name, sink);
166
+ const inner = this.expandList(bundle.specs, [...trail, spec.name], sink);
167
+ warnOnFlattenedOffsets(spec, inner, sink.warnings);
168
+ for (const member of inner) out.push(overlay(member, spec));
169
+ }
170
+ return out;
171
+ }
172
+ /**
173
+ * Name an unregistered effect that is one typo away from a bundle.
174
+ *
175
+ * `compile.ts` already reports every unknown name, with a suggestion drawn from the catalog — a
176
+ * misspelled *bundle* is invisible in that list, so this adds the one fact that message cannot
177
+ * carry. Only for names the registry does not know, or a real effect one edit away from a badly
178
+ * chosen bundle name would be reported as a mistake.
179
+ *
180
+ * @complexity O(b·n) time in bundle count and name length; O(n) space.
181
+ * @overallScore 100
182
+ */
183
+ noteNearMiss(name, warnings) {
184
+ if (this.registry.has(name)) return;
185
+ const hit = suggest(name, [...this.bundles.keys()]);
186
+ if (hit) {
187
+ warnings.push(`"${name}" is not a registered effect \u2014 did you mean the bundle "${hit}"?`);
188
+ }
189
+ }
190
+ };
191
+ function readBundle(name, source, el, reporter) {
192
+ const parsed = parse(source);
193
+ for (const warning of parsed.warnings) reporter.warn(`in bundle "${name}": ${warning}`, el);
194
+ if (parsed.specs.length === 0) {
195
+ reporter.warn(`bundle "${name}" names no effect \u2014 elements that use it animate nothing`, el);
196
+ }
197
+ const group = ["cascade", "spread", "order", "cols", "along"].filter((key) => parsed[key] !== void 0).map((key) => `"${key}:"`).join(" and ");
198
+ if (group) {
199
+ reporter.warn(
200
+ `bundle "${name}" declares ${group} \u2014 a stagger group is read from the group element's own attribute, which never sees a bundle, so it is dropped; write it on the group element itself`,
201
+ el
202
+ );
203
+ }
204
+ return { source, specs: parsed.specs, hoists: parsed };
205
+ }
206
+ function takeHoists(bundle, name, sink) {
207
+ for (const key of HOIST_KEYS) {
208
+ if (absorbHoist(sink.hoists, bundle.hoists, key) !== "clash") continue;
209
+ const label = HOIST_LABELS[key];
210
+ const kept = String(sink.hoists[key]);
211
+ sink.warnings.push(
212
+ `bundle "${name}" sets "${label}:${String(bundle.hoists[key])}", which conflicts with "${label}:${kept}" from an earlier bundle \u2014 the first wins`
213
+ );
214
+ }
215
+ }
216
+ function cycleWarning(trail, name) {
217
+ const path = [...trail, name].join(" \u2192 ");
218
+ return `bundle "${name}" is defined in terms of itself (${path}) \u2014 the reference is dropped`;
219
+ }
220
+ function definitionsIn(root) {
221
+ const selector = `[${ATTR.define}]`;
222
+ const found = [...root.querySelectorAll(selector)];
223
+ const self = root.nodeType === 1 ? root : null;
224
+ if (self?.matches(selector)) found.unshift(self);
225
+ return found;
226
+ }
227
+ function absorbHoist(into, from, key) {
228
+ const value = from[key];
229
+ if (value === void 0) return "kept";
230
+ const current = into[key];
231
+ if (current === void 0) {
232
+ into[key] = value;
233
+ return "taken";
234
+ }
235
+ return current === value ? "kept" : "clash";
236
+ }
237
+ function overlay(member, ref) {
238
+ return {
239
+ ...member,
240
+ duration: ref.duration ?? member.duration,
241
+ delay: ref.delay ?? member.delay,
242
+ easing: ref.easing ?? member.easing,
243
+ at: ref.at ?? member.at,
244
+ gate: ref.gate ?? member.gate,
245
+ repeat: ref.repeat ?? member.repeat,
246
+ yoyo: ref.yoyo ?? member.yoyo,
247
+ params: { ...member.params, ...ref.params },
248
+ hoists: overlayHoists(member.hoists, ref.hoists)
249
+ };
250
+ }
251
+ function overlayHoists(member, ref) {
252
+ if (!member && !ref) return void 0;
253
+ const merged = { ...member };
254
+ for (const key of SEGMENT_HOIST_KEYS) {
255
+ const value = ref?.[key];
256
+ if (value !== void 0) merged[key] = value;
257
+ }
258
+ return merged;
259
+ }
260
+ function warnOnFlattenedOffsets(ref, inner, warnings) {
261
+ if (ref.at === void 0 || !inner.some((member) => member.at !== void 0)) return;
262
+ warnings.push(
263
+ `"at:${ref.at}" on "${ref.name}" overrides the offsets the bundle sets on its own segments \u2014 they now all start at the same position`
264
+ );
265
+ }
266
+
267
+ // src/core/callback.ts
268
+ function bindCallback(request) {
269
+ const { el, name, reporter, signal } = request;
270
+ const handler = (event) => {
271
+ if (event.target !== el) return;
272
+ const fn = resolveGlobalFunction(el, name);
273
+ if (!fn) {
274
+ reporter.warn(
275
+ `func:${name} \u2014 no global function named "${name}". A function declaration (\`function ${name}() {}\`) or an explicit \`window.${name} = \u2026\` creates one; \`const\` and \`let\` deliberately do not, and neither does a bundled or \`type="module"\` script.`,
276
+ el
277
+ );
278
+ return;
279
+ }
280
+ Reflect.apply(fn, el, [event]);
281
+ };
282
+ el.addEventListener(KUI_EVENT.finish, handler);
283
+ signal.addEventListener("abort", () => el.removeEventListener(KUI_EVENT.finish, handler));
284
+ }
285
+ function resolveGlobalFunction(el, name) {
286
+ const scope = el.ownerDocument?.defaultView ?? globalThis;
287
+ if (!Object.hasOwn(scope, name)) return void 0;
288
+ const candidate = scope[name];
289
+ return typeof candidate === "function" ? candidate : void 0;
290
+ }
291
+
292
+ // src/core/capabilities.ts
293
+ function supports(property, value) {
294
+ if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
295
+ try {
296
+ return CSS.supports(property, value);
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ var cached;
302
+ function detect(force = false) {
303
+ if (cached && !force) return cached;
304
+ cached = {
305
+ viewTimeline: supports("animation-timeline", "view()"),
306
+ scrollTimeline: supports("animation-timeline", "scroll()"),
307
+ animationRange: supports("animation-range", "entry 0% cover 30%"),
308
+ // `translate`/`rotate`/`scale` as independent properties is what makes the channel model
309
+ // possible at all — under the `transform` shorthand every effect would collide.
310
+ individualTransforms: supports("translate", "0 10px") && supports("scale", "1.1"),
311
+ scrollTimelineName: supports("scroll-timeline-name", "--x"),
312
+ viewTransitions: typeof document !== "undefined" && "startViewTransition" in document,
313
+ intersectionObserver: typeof IntersectionObserver !== "undefined",
314
+ reducedMotion: typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches,
315
+ motionPath: supports("offset-path", 'path("M 0 0")')
316
+ };
317
+ return cached;
318
+ }
319
+ var CHANNEL_REQUIREMENTS = [
320
+ {
321
+ channel: "offset",
322
+ capability: "motionPath",
323
+ message: "this browser does not support CSS Motion Path (offset-path), so the element will not travel \u2014 the animation still runs and completes, it just has no path to follow"
324
+ }
325
+ ];
326
+ function unsupportedChannelWarnings(channels, capabilities) {
327
+ return CHANNEL_REQUIREMENTS.filter(
328
+ (entry) => channels.includes(entry.channel) && !capabilities[entry.capability]
329
+ ).map((entry) => entry.message);
330
+ }
331
+
332
+ // src/core/channels.ts
333
+ var INDEPENDENT_PHASES = /* @__PURE__ */ new Set(["entrance|state", "exit|state"]);
334
+ function phasePair(a, b) {
335
+ return a < b ? `${a}|${b}` : `${b}|${a}`;
336
+ }
337
+ function independentPhases(a, b) {
338
+ if (!a.phase || !b.phase) return false;
339
+ return INDEPENDENT_PHASES.has(phasePair(a.phase, b.phase));
340
+ }
341
+ var CHANNEL_COMPOSITION = {
342
+ translate: "add"
343
+ };
344
+ function additiveChannels(channels) {
345
+ return channels.length > 0 && channels.every((channel) => CHANNEL_COMPOSITION[channel] === "add");
346
+ }
347
+ function additiveResolution(claims, conflicts) {
348
+ if (conflicts.some((conflict) => conflict.effects[0] === conflict.effects[1])) return void 0;
349
+ const contested = new Set(conflicts.map((conflict) => conflict.channel));
350
+ const involved = /* @__PURE__ */ new Set();
351
+ claims.forEach((claim2, index) => {
352
+ if (claim2.channels.some((channel) => contested.has(channel))) involved.add(index);
353
+ });
354
+ for (const index of involved) if (!claims[index].additive) return void 0;
355
+ return involved;
356
+ }
357
+ function findConflicts(claims) {
358
+ const conflicts = [];
359
+ const seen = /* @__PURE__ */ new Map();
360
+ for (const claim2 of claims) {
361
+ for (const channel of claim2.channels) {
362
+ const claimants = seen.get(channel) ?? [];
363
+ const clash = claimants.find(
364
+ // Same effect name twice in one list claims the channel against itself; still a conflict,
365
+ // and a more likely author typo than a deliberate choice. Phase cannot exempt that pair
366
+ // either, because a phase is only ever independent of a *different* one.
367
+ (other) => gatesOverlap(other.gate, claim2.gate) && !independentPhases(other, claim2)
368
+ );
369
+ if (clash) conflicts.push({ channel, effects: [clash.name, claim2.name] });
370
+ claimants.push(claim2);
371
+ seen.set(channel, claimants);
372
+ }
373
+ }
374
+ return conflicts;
375
+ }
376
+ function deliveryClobbers(claims) {
377
+ const stylesheet = claims.filter((claim2) => claim2.delivery === "stylesheet-animation");
378
+ if (stylesheet.length === 0) return [];
379
+ const inline = claims.find((claim2) => claim2.inlineAnimation && !claim2.delivery);
380
+ if (!inline) return [];
381
+ return stylesheet.map(
382
+ (claim2) => `"${inline.name}" writes an inline animation, which outranks the stylesheet rule "${claim2.name}" delivers its motion from`
383
+ );
384
+ }
385
+ function describeConflicts(conflicts) {
386
+ return conflicts.map((c) => `"${c.effects[0]}" and "${c.effects[1]}" both animate ${c.channel}`).join("; ");
387
+ }
388
+
389
+ // src/core/sequence.ts
390
+ var DURATION_FALLBACK = "600ms";
391
+ var DELAY_FALLBACK = "0ms";
392
+ function durationExpression(duration, primitiveId) {
393
+ return duration ?? `var(${timingProperty(primitiveId, "duration")}, ${DURATION_FALLBACK})`;
394
+ }
395
+ function delayExpression(delay, primitiveId) {
396
+ return delay ?? `var(${timingProperty(primitiveId, "delay")}, ${DELAY_FALLBACK})`;
397
+ }
398
+ var AT_ANCHOR = /^(with|after)/i;
399
+ var AT_OFFSET = /^([+-])(\d+(?:\.\d+)?|\.\d+)(ms|s)$/i;
400
+ var BARE_TIME = /^(?:\d+(?:\.\d+)?|\.\d+)(?:ms|s)$/i;
401
+ var PROGRESS_DRIVEN = /* @__PURE__ */ new Set(["view", "scroll"]);
402
+ function parsePosition(raw) {
403
+ const value = raw.trim();
404
+ const named = AT_ANCHOR.exec(value);
405
+ const anchor = named?.[1]?.toLowerCase() === "with" ? "start" : "end";
406
+ const offset = named ? value.slice(named[0].length) : value;
407
+ if (offset === "") {
408
+ if (!named) return { ok: false, reason: refusalReason(value) };
409
+ return { ok: true, position: { anchor, offsetMs: 0, term: "" } };
410
+ }
411
+ const parts = AT_OFFSET.exec(offset);
412
+ if (!parts) return { ok: false, reason: refusalReason(value) };
413
+ const [, sign, amount, unit] = parts;
414
+ const time = `${amount}${unit}`;
415
+ const magnitude = toMilliseconds(time, 0);
416
+ return {
417
+ ok: true,
418
+ position: {
419
+ anchor,
420
+ offsetMs: sign === "-" ? -magnitude : magnitude,
421
+ // Spaces around the operator are required by `calc()`, not decoration: `calc(600ms -200ms)`
422
+ // is a syntax error, and a browser drops the whole declaration rather than reporting it.
423
+ term: ` ${sign} ${time}`
424
+ }
425
+ };
426
+ }
427
+ function refusalReason(value) {
428
+ if (BARE_TIME.test(value)) {
429
+ return `at:"${value}" is an absolute position, which is only another spelling of delay:${value} \u2014 write at:+${value} to start that long after the previous effect ends, at:-${value} to overlap it by that much, or at:with to start alongside it`;
430
+ }
431
+ return `at:"${value}" is not a position \u2014 expected at:with, at:after, or a signed time such as at:-200ms, at:+100ms or at:with+150ms`;
432
+ }
433
+ function resolveSequence(members, timeline, warn) {
434
+ const chain = { members, steps: [], warn };
435
+ let warnedAboutTimeline = false;
436
+ for (const [index, member] of members.entries()) {
437
+ if (member.at === void 0) {
438
+ chain.steps.push(unsequenced(member));
439
+ continue;
440
+ }
441
+ if (!warnedAboutTimeline && PROGRESS_DRIVEN.has(timeline)) {
442
+ warnedAboutTimeline = true;
443
+ warn(timelineRefusal(timeline));
444
+ }
445
+ chain.steps.push(place(chain, index));
446
+ }
447
+ return chain.steps;
448
+ }
449
+ function unsequenced(member) {
450
+ return {
451
+ delayExpr: delayExpression(member.delay, member.primitiveId),
452
+ // Falls back to the same `0ms` the compiled `var()` does. An unreadable delay can only reach
453
+ // here from a direct caller — `compile` screens its candidates with `isReadableTime`, and a
454
+ // positional delay was already matched against the parser's own time pattern.
455
+ delayMs: timeMs(member.delay ?? member.cascadeDelay ?? DELAY_FALLBACK) ?? 0,
456
+ sequenced: false
457
+ };
458
+ }
459
+ function place(chain, index) {
460
+ const member = chain.members[index];
461
+ const own = unsequenced(member);
462
+ const parsed = parsePosition(member.at);
463
+ if (!parsed.ok) {
464
+ chain.warn(`"${member.name}": ${parsed.reason}`);
465
+ return own;
466
+ }
467
+ const refused = refusalFor(chain, index);
468
+ if (refused) {
469
+ chain.warn(refused);
470
+ return own;
471
+ }
472
+ if (member.delay !== void 0) {
473
+ chain.warn(
474
+ `"${member.name}" has both a positional delay (${member.delay}) and at:"${member.at}" \u2014 the delay is ignored, because at: positions the effect itself`
475
+ );
476
+ }
477
+ return follow(chain, index, parsed.position, own);
478
+ }
479
+ function refusalFor(chain, index) {
480
+ const member = chain.members[index];
481
+ if (index === 0) {
482
+ return `"${member.name}" is the first effect in the list, so at:"${member.at}" has nothing to be relative to \u2014 using its own delay instead`;
483
+ }
484
+ if (!member.positionable) {
485
+ return `"${member.name}" is rendered in JavaScript and declares no "delay", so at:"${member.at}" cannot position it \u2014 it starts with the rest of the list`;
486
+ }
487
+ return null;
488
+ }
489
+ function follow(chain, index, position, own) {
490
+ const previous = chain.members[index - 1];
491
+ const anchor = chain.steps[index - 1];
492
+ if (position.anchor === "start") {
493
+ return {
494
+ delayExpr: `${anchor.delayExpr}${position.term}`,
495
+ delayMs: anchor.delayMs + position.offsetMs,
496
+ sequenced: true
497
+ };
498
+ }
499
+ if (isInfiniteRepeat(previous.repeat)) return refuseEndless(chain, index, own);
500
+ const durationMs = timeMs(previous.duration ?? previous.cascadeDuration ?? "");
501
+ if (durationMs === void 0) {
502
+ return refuseUnmeasurable(chain, index, own);
503
+ }
504
+ const played = playbackExpression(
505
+ durationExpression(previous.duration, previous.primitiveId),
506
+ previous.repeat
507
+ );
508
+ return {
509
+ delayExpr: `${anchor.delayExpr} + ${played}${position.term}`,
510
+ delayMs: anchor.delayMs + durationMs * (repeatCount(previous.repeat) ?? 1) + position.offsetMs,
511
+ sequenced: true
512
+ };
513
+ }
514
+ function refuseUnmeasurable(chain, index, own) {
515
+ const member = chain.members[index];
516
+ const previous = chain.members[index - 1];
517
+ chain.warn(
518
+ `cannot start "${member.name}" relative to the end of "${previous.name}": "${previous.name}" has no readable duration, so it has no end to measure from \u2014 give it an explicit duration, or use at:with to start alongside it instead`
519
+ );
520
+ return own;
521
+ }
522
+ function refuseEndless(chain, index, own) {
523
+ const member = chain.members[index];
524
+ const previous = chain.members[index - 1];
525
+ chain.warn(
526
+ `cannot start "${member.name}" relative to the end of "${previous.name}": "${previous.name}" has repeat:infinite, so it never ends \u2014 use at:with to start alongside it, or give it a finite repeat count`
527
+ );
528
+ return own;
529
+ }
530
+ function timelineRefusal(timeline) {
531
+ return `at: compiles to an animation-delay, and a "${timeline}" timeline is driven by scroll position rather than a clock, so it ignores one \u2014 position a scroll-driven effect with a range instead (the trailing tokens of data-kui-timeline, e.g. data-kui-timeline="${timeline} entry 0% cover 60%"). The delay still applies where the browser has no scroll-driven animations and the effect degrades to a clock.`;
532
+ }
533
+ function timeMs(value) {
534
+ const ms = toMilliseconds(value, Number.NaN);
535
+ return Number.isFinite(ms) ? ms : void 0;
536
+ }
537
+ function isReadableTime(value) {
538
+ return timeMs(value) !== void 0;
539
+ }
540
+
541
+ // src/core/declarations.ts
542
+ function emptyTracks() {
543
+ return {
544
+ names: [],
545
+ keyframes: [],
546
+ durations: [],
547
+ delays: [],
548
+ ends: [],
549
+ easings: [],
550
+ iterationCounts: [],
551
+ directions: [],
552
+ compositions: []
553
+ };
554
+ }
555
+ function keyframesFor(entry) {
556
+ const { preset } = entry.resolved;
557
+ return entry.variant?.keyframes ?? [preset.keyframes ?? `kui-${preset.name}`];
558
+ }
559
+ function iterationCountProperty(presetName) {
560
+ return `--kui-fx-${presetName}-iterations`;
561
+ }
562
+ function pushTrack(tracks, entry, step) {
563
+ const { spec, resolved } = entry;
564
+ const id = resolved.primitive.id;
565
+ const duration = durationExpression(spec.duration, id);
566
+ const delay = staggerDelay(step.delayExpr);
567
+ const end = `${step.delayExpr} + ${playbackExpression(duration, spec.repeat)}`;
568
+ const easing = easingValue(spec.easing, id);
569
+ const iterations = spec.repeat ?? `var(${iterationCountProperty(resolved.preset.name)}, 1)`;
570
+ const direction = directionValue(spec.yoyo);
571
+ const composition = entry.composite ?? "replace";
572
+ for (const name of keyframesFor(entry)) {
573
+ tracks.names.push(gatedAnimationName(name, spec.gate));
574
+ tracks.keyframes.push(name);
575
+ tracks.durations.push(duration);
576
+ tracks.delays.push(delay);
577
+ tracks.ends.push(end);
578
+ tracks.easings.push(easing);
579
+ tracks.iterationCounts.push(iterations);
580
+ tracks.directions.push(direction);
581
+ tracks.compositions.push(composition);
582
+ }
583
+ }
584
+ function pushTransitions(segments, owners, entry, warnings) {
585
+ const { spec, resolved } = entry;
586
+ const { preset, primitive } = resolved;
587
+ for (const segment of preset.transitions ?? []) {
588
+ const previousOwner = owners.get(segment.property);
589
+ if (previousOwner !== void 0 && previousOwner !== preset.name) {
590
+ warnings.push(
591
+ `"${previousOwner}" and "${preset.name}" both transition ${segment.property} \u2014 "${preset.name}" wins (last in the list)`
592
+ );
593
+ }
594
+ owners.set(segment.property, preset.name);
595
+ const duration = segment.duration ?? durationExpression(spec.duration, primitive.id);
596
+ const easing = segment.easing ?? easingValue(spec.easing, primitive.id);
597
+ const delay = `var(--kui-tx-delay-${preset.name}, 0ms)`;
598
+ segments.push(`${segment.property} ${duration} ${easing} ${delay}`);
599
+ }
600
+ }
601
+ function declarationsFor(tracks, timeline) {
602
+ if (tracks.names.length === 0) return {};
603
+ const delays = timeline === "pin" ? pinnedDelays(tracks) : tracks.delays.map(wrapped);
604
+ const declarations = {
605
+ "animation-name": tracks.names.join(", "),
606
+ "animation-duration": tracks.durations.join(", "),
607
+ "animation-delay": delays.join(", "),
608
+ "animation-timing-function": tracks.easings.join(", "),
609
+ "animation-iteration-count": tracks.iterationCounts.join(", "),
610
+ "animation-fill-mode": tracks.names.map(() => "both").join(", ")
611
+ };
612
+ if (tracks.directions.some((value) => value !== "normal")) {
613
+ declarations["animation-direction"] = tracks.directions.join(", ");
614
+ }
615
+ if (tracks.compositions.some((value) => value !== "replace")) {
616
+ declarations["animation-composition"] = tracks.compositions.join(", ");
617
+ }
618
+ return declarations;
619
+ }
620
+ function staggerDelay(base) {
621
+ return `${base} + var(--kui-i, 0) * var(--kui-stagger, 0ms)`;
622
+ }
623
+ function wrapped(sum) {
624
+ return `calc(${sum})`;
625
+ }
626
+ function pinnedDelays(tracks) {
627
+ const ends = [...new Set(tracks.ends)];
628
+ const span = ends.length === 1 ? ends[0] : `max(${ends.join(", ")})`;
629
+ const head = `${span} + (var(--kui-stagger-count, 1) - 1) * var(--kui-stagger, 0ms)`;
630
+ return tracks.delays.map((delay) => wrapped(`${delay} - var(--kui-progress, 0) * (${head})`));
631
+ }
632
+ function easingValue(easing, primitiveId) {
633
+ if (!easing) return `var(${timingProperty(primitiveId, "ease")}, ease-out)`;
634
+ return cssEasingValue(easing);
635
+ }
636
+
637
+ // src/core/element-config.ts
638
+ var TIMELINES = /* @__PURE__ */ new Set(["time", "view", "scroll", "pointer", "pin"]);
639
+ function readAttributes(el) {
640
+ return {
641
+ source: el.getAttribute(ATTR.source) ?? "",
642
+ on: el.getAttribute(ATTR.on),
643
+ timeline: el.getAttribute(ATTR.timeline),
644
+ threshold: el.getAttribute(ATTR.threshold)
645
+ };
646
+ }
647
+ function resolveConfig(attributes, parsed) {
648
+ const rawTimeline = parsed.timeline ?? attributes.timeline ?? "time";
649
+ const [head = "time", ...rest] = rawTimeline.trim().split(/\s+/);
650
+ const longhand = parseActivationAttribute(attributes.on);
651
+ const authored = parsed.activation ?? longhand.activation;
652
+ return {
653
+ activation: authored ?? "enter",
654
+ activationAuthored: authored !== void 0,
655
+ ...sourceFromLonghand({ parsed, longhand }),
656
+ // Re-parsed rather than threaded down, and its warnings dropped: `parse.ts` has already
657
+ // reported every one of them against this same element through `validateToggleActions`, so
658
+ // forwarding them would double each diagnostic. There is no longhand attribute to merge with —
659
+ // `actions:` refines `on:` and has only the inline spelling.
660
+ ...parsed.actions === void 0 ? {} : { actions: parseToggleActions(parsed.actions) },
661
+ timeline: TIMELINES.has(head) ? head : "time",
662
+ range: rest.join(" "),
663
+ threshold: parsed.threshold ?? attributes.threshold ?? "0%"
664
+ };
665
+ }
666
+ function resolveTimelineHead(raw, fallback) {
667
+ if (raw === void 0) return fallback;
668
+ const [head] = raw.trim().split(/\s+/);
669
+ return TIMELINES.has(head) ? head : fallback;
670
+ }
671
+ function resolveGroupConfig(base, hoists) {
672
+ if (!hoists) return base;
673
+ const authored = hoists.activation;
674
+ const config = {
675
+ ...base,
676
+ activation: authored ?? base.activation,
677
+ activationAuthored: authored !== void 0 || base.activationAuthored
678
+ };
679
+ if (hoists.actions !== void 0) config.actions = parseToggleActions(hoists.actions);
680
+ if (hoists.timeline !== void 0) {
681
+ const [head = "time", ...rest] = hoists.timeline.trim().split(/\s+/);
682
+ config.timeline = TIMELINES.has(head) ? head : base.timeline;
683
+ config.range = rest.join(" ");
684
+ }
685
+ if (hoists.threshold !== void 0) config.threshold = hoists.threshold;
686
+ return config;
687
+ }
688
+ function sourceFromLonghand({
689
+ parsed,
690
+ longhand
691
+ }) {
692
+ if (parsed.activation !== void 0 || longhand.from === void 0) return {};
693
+ return { activationSource: longhand.from };
694
+ }
695
+
696
+ // src/core/host-facts.ts
697
+ var RM_RANK = { shorten: 0, crossfade: 1, disable: 2 };
698
+ function resolveDefaultActivation(claims) {
699
+ const declared = claims.map((claim2) => claim2.defaultActivation);
700
+ const firstWins = declared.find((value) => value !== void 0);
701
+ const entrance = claims.findIndex((claim2) => claim2.phase === "entrance");
702
+ if (firstWins === void 0 || entrance === -1) return firstWins;
703
+ return declared[entrance] ?? "enter";
704
+ }
705
+ function mergeHostFacts(targets) {
706
+ let reducedMotion = "shorten";
707
+ for (const { plan } of targets) reducedMotion = strictestPolicy(reducedMotion, plan.reducedMotion);
708
+ for (const { plan } of targets) plan.reducedMotion = reducedMotion;
709
+ }
710
+ function resolvedPolicy(declared, authored, warnings) {
711
+ if (authored === void 0) return declared;
712
+ if (RM_RANK[authored] < RM_RANK[declared]) {
713
+ warnings.push(
714
+ `"rm:${authored}" is weaker than the "${declared}" these effects declare \u2014 keeping "${declared}" (rm: may only strengthen the reduced-motion policy)`
715
+ );
716
+ return declared;
717
+ }
718
+ return authored;
719
+ }
720
+ function intersect(accumulated, supported) {
721
+ if (!accumulated) return [...supported];
722
+ return accumulated.filter((value) => supported.includes(value));
723
+ }
724
+ function strictestPolicy(a, b) {
725
+ return RM_RANK[b] > RM_RANK[a] ? b : a;
726
+ }
727
+
728
+ // src/core/unquoted-selectors.ts
729
+ var SELECTOR_START = /^[.#[*:]/;
730
+ var SELECTOR_COMBINATOR = /[>~+]/;
731
+ function looksLikeSelectorFragment(name) {
732
+ return SELECTOR_START.test(name) || SELECTOR_COMBINATOR.test(name);
733
+ }
734
+ function findUnquotedSelectors(specs, registry) {
735
+ const found = [];
736
+ for (const [i, spec] of specs.entries()) {
737
+ const target = specs[i - 1]?.params.target;
738
+ if (!target || registry.has(spec.name) || !looksLikeSelectorFragment(spec.name)) continue;
739
+ found.push({ previous: target, segment: spec.name });
740
+ }
741
+ return found;
742
+ }
743
+ function unquotedSelectorWarning(found) {
744
+ return `target:${found.previous} looks like it continues into "${found.segment}", but a comma always starts a new effect segment \u2014 a target: selector containing a comma must be quoted, e.g. target:'${found.previous}, ${found.segment}'`;
745
+ }
746
+
747
+ // src/core/compile.ts
748
+ function channelsFor(entry) {
749
+ const declared = entry.resolved.primitive.channels;
750
+ if (!entry.variant?.channels) return declared;
751
+ return [...declared, ...entry.variant.channels];
752
+ }
753
+ function authoredParams(entry) {
754
+ return entry.variant?.params ?? entry.spec.params;
755
+ }
756
+ function schemaFor(entry) {
757
+ const extra = entry.variant?.schema;
758
+ if (!extra) return entry.resolved.primitive.parameters;
759
+ return { ...entry.resolved.primitive.parameters, ...extra };
760
+ }
761
+ function compileTargets(parsed, registry, timeline) {
762
+ const warnings = [...parsed.warnings];
763
+ const unquoted = findUnquotedSelectors(parsed.specs, registry);
764
+ for (const found of unquoted) warnings.push(unquotedSelectorWarning(found));
765
+ const suspects = unquoted.length > 0 ? { suspectSelectors: unquoted.map((found) => found.segment) } : {};
766
+ const { entries, unknown } = resolveEntries(parsed.specs, registry, warnings);
767
+ if (entries.length === 0) {
768
+ return {
769
+ targets: [{ selector: "", scope: "self", specs: [], plan: emptyPlan(unknown, []) }],
770
+ warnings,
771
+ ...suspects
772
+ };
773
+ }
774
+ const sanitized = entries.map(
775
+ (entry) => refusePlayback(refuseContainerGate(entry, warnings), timeline, warnings)
776
+ );
777
+ const steps = resolveSequence(sanitized.map(memberFor), timeline, (m) => warnings.push(m));
778
+ const sequenced = sanitized.map((entry, index) => ({ ...entry, step: steps[index] }));
779
+ warnCrossGroupSequencing(sequenced, parsed.activation, warnings);
780
+ const targets = partitionByTarget(sequenced).map(({ selector, scope, entries: group }) => {
781
+ const groupWarnings = [];
782
+ const composed = resolveComposition(group, registry, groupWarnings);
783
+ const groupHoists = mergeGroupHoists(group, groupWarnings);
784
+ const groupTimeline = resolveTimelineHead(groupHoists?.timeline, timeline);
785
+ const plan = buildPlan(composed, groupTimeline, unknown, groupWarnings);
786
+ plan.defaultActivation = resolveDefaultActivation(
787
+ composed.map((entry) => ({
788
+ phase: phaseOf(entry),
789
+ defaultActivation: entry.resolved.primitive.defaultActivation
790
+ }))
791
+ );
792
+ return { selector, scope, hoists: groupHoists, specs: group.map((entry) => entry.spec), plan };
793
+ });
794
+ mergeHostFacts(targets);
795
+ const reducedMotion = resolvedPolicy(targets[0].plan.reducedMotion, parsed.rm, warnings);
796
+ for (const target of targets) target.plan.reducedMotion = reducedMotion;
797
+ return { targets, warnings, ...suspects };
798
+ }
799
+ function scopeHoists(parsed, registry) {
800
+ if (!parsed.specs.some((spec) => spec.hoists)) return parsed;
801
+ const warnings = [...parsed.warnings];
802
+ const folded = { ...parsed, warnings };
803
+ const specs = parsed.specs.map((spec) => {
804
+ if (!spec.hoists || !staysOnHost(spec, registry)) return spec;
805
+ for (const key of SEGMENT_HOIST_KEYS) {
806
+ const value = spec.hoists[key];
807
+ if (value === void 0) continue;
808
+ assignOnce(folded, key, value, HOIST_LABELS2[key]);
809
+ }
810
+ const rest = { ...spec };
811
+ delete rest.hoists;
812
+ return rest;
813
+ });
814
+ return { ...folded, specs };
815
+ }
816
+ function staysOnHost(spec, registry) {
817
+ const resolved = registry.resolve(spec.name);
818
+ if (!resolved) return true;
819
+ return Object.hasOwn(resolved.primitive.parameters, "target");
820
+ }
821
+ var HOIST_LABELS2 = {
822
+ activation: "activations",
823
+ actions: "crossing actions",
824
+ timeline: "timelines",
825
+ threshold: "thresholds",
826
+ cascade: "stagger steps",
827
+ spread: "stagger budgets",
828
+ order: "stagger orders",
829
+ cols: "stagger column counts",
830
+ along: "stagger axes"
831
+ };
832
+ function mergeGroupHoists(entries, warnings) {
833
+ let merged;
834
+ for (const entry of entries) {
835
+ if (!entry.spec.hoists) continue;
836
+ merged = absorbGroupHoists(merged ?? {}, entry.spec.hoists, warnings);
837
+ }
838
+ return merged;
839
+ }
840
+ function absorbGroupHoists(merged, hoists, warnings) {
841
+ for (const key of SEGMENT_HOIST_KEYS) {
842
+ const value = hoists[key];
843
+ if (value === void 0) continue;
844
+ const current = merged[key];
845
+ if (current === void 0) merged[key] = value;
846
+ else if (current !== value) {
847
+ warnings.push(
848
+ `conflicting ${HOIST_LABELS2[key]} "${current}" and "${value}" in one target: group \u2014 the first wins`
849
+ );
850
+ }
851
+ }
852
+ return merged;
853
+ }
854
+ function warnCrossGroupSequencing(sequenced, elementActivation, warnings) {
855
+ for (let i = 1; i < sequenced.length; i++) {
856
+ const entry = sequenced[i];
857
+ if (!entry.step.sequenced) continue;
858
+ const previous = sequenced[i - 1];
859
+ if (groupKeyOf(entry) === groupKeyOf(previous)) continue;
860
+ const ownTrigger = entry.spec.hoists?.activation ?? elementActivation;
861
+ const previousTrigger = previous.spec.hoists?.activation ?? elementActivation;
862
+ if (ownTrigger === previousTrigger) continue;
863
+ warnings.push(
864
+ `"${entry.spec.name}"'s at:"${entry.spec.at}" is relative to "${previous.spec.name}", which targets a different element and may start on a different trigger \u2014 the offset is relative to each one's own start, not a shared clock`
865
+ );
866
+ }
867
+ }
868
+ function groupKeyOf(entry) {
869
+ return `${entry.scope ?? "self"} ${entry.target ?? ""}`;
870
+ }
871
+ function partitionByTarget(entries) {
872
+ const byKey = /* @__PURE__ */ new Map();
873
+ const order = [];
874
+ for (const entry of entries) {
875
+ const selector = entry.target ?? "";
876
+ const scope = entry.scope ?? "self";
877
+ const key = `${scope} ${selector}`;
878
+ let group = byKey.get(key);
879
+ if (!group) {
880
+ group = { selector, scope, entries: [] };
881
+ byKey.set(key, group);
882
+ order.push(group);
883
+ }
884
+ group.entries.push(entry);
885
+ }
886
+ const hostIndex = order.findIndex((group) => group.selector === "");
887
+ if (hostIndex > 0) {
888
+ const [host] = order.splice(hostIndex, 1);
889
+ order.unshift(host);
890
+ }
891
+ return order;
892
+ }
893
+ function emptyPlan(unknown, warnings) {
894
+ return {
895
+ fxNames: [],
896
+ vars: {},
897
+ declarations: {},
898
+ keyframeNames: [],
899
+ jsEffects: [],
900
+ unknown,
901
+ reducedMotion: "shorten",
902
+ supportedActivations: [],
903
+ supportedTimelines: [],
904
+ channels: [],
905
+ warnings
906
+ };
907
+ }
908
+ function resolveEntries(specs, registry, warnings) {
909
+ const entries = [];
910
+ const unknown = [];
911
+ for (const spec of specs) {
912
+ const resolved = registry.resolve(spec.name);
913
+ if (!resolved) {
914
+ unknown.push(spec.name);
915
+ warnUnknownEffect(spec.name, registry, warnings);
916
+ continue;
917
+ }
918
+ entries.push(entryFor(spec, resolved, warnings));
919
+ }
920
+ return { entries, unknown };
921
+ }
922
+ function entryFor(spec, resolved, warnings) {
923
+ const lifted = liftTarget(spec, resolved, warnings);
924
+ const variant = resolved.primitive.variantFor?.(lifted.spec, (m) => warnings.push(m));
925
+ const entry = variant ? { spec: lifted.spec, resolved, variant } : { spec: lifted.spec, resolved };
926
+ if (lifted.target !== void 0) {
927
+ entry.target = lifted.target;
928
+ entry.scope = lifted.scope;
929
+ }
930
+ return entry;
931
+ }
932
+ function warnUnknownEffect(name, registry, warnings) {
933
+ const hint = suggest(name, registry.names());
934
+ const suffix = hint ? ` \u2014 did you mean "${hint}"?` : "";
935
+ warnings.push(`unknown effect "${name}"${suffix}`);
936
+ }
937
+ function liftTarget(spec, resolved, warnings) {
938
+ if (Object.hasOwn(resolved.primitive.parameters, "target")) return { spec };
939
+ const target = spec.params.target;
940
+ if (!target) return { spec };
941
+ const rest = { ...spec.params };
942
+ const authoredScope = rest.scope;
943
+ delete rest.target;
944
+ delete rest.scope;
945
+ const stripped = { ...spec, params: rest };
946
+ if (resolved.preset.requiresOwnSubtree) {
947
+ warnings.push(
948
+ `"${resolved.preset.name}" cannot be retargeted \u2014 its CSS reaches past the animated element itself, so "target:${target}" is dropped and it stays on the host`
949
+ );
950
+ return { spec: stripped };
951
+ }
952
+ const scope = authoredScope === "page" ? "page" : "self";
953
+ return { spec: stripped, target, scope };
954
+ }
955
+ function refuseContainerGate(entry, warnings) {
956
+ const { spec, resolved } = entry;
957
+ if (resolved.primitive.renderer === "css-keyframes") return entry;
958
+ if (!spec.gate?.wide && !spec.gate?.narrow) return entry;
959
+ warnings.push(
960
+ `"${spec.name}" ignores "wide:"/"narrow:" \u2014 container gates are not supported on JavaScript-rendered effects yet, so it runs unconditionally`
961
+ );
962
+ const { above, below } = spec.gate;
963
+ const gate = above || below ? { above, below } : void 0;
964
+ return { ...entry, spec: { ...spec, gate } };
965
+ }
966
+ function refusePlayback(entry, timeline, warnings) {
967
+ const { spec, resolved } = entry;
968
+ if (spec.repeat === void 0 && spec.yoyo === void 0) return entry;
969
+ const { repeat, yoyo, warnings: raised } = resolvePlayback({
970
+ name: resolved.preset.name,
971
+ renderer: resolved.primitive.renderer,
972
+ cloak: resolved.preset.cloak,
973
+ timeline,
974
+ repeat: spec.repeat,
975
+ yoyo: spec.yoyo
976
+ });
977
+ warnings.push(...raised);
978
+ return { ...entry, spec: { ...spec, repeat, yoyo } };
979
+ }
980
+ function phaseOf(entry) {
981
+ if (entry.spec.repeat === "infinite") return "idle";
982
+ const { preset } = entry.resolved;
983
+ if (preset.phase) return preset.phase;
984
+ if (preset.transitions?.length) return "state";
985
+ return void 0;
986
+ }
987
+ function additivelyComposable(entry) {
988
+ if (entry.resolved.primitive.renderer !== "css-keyframes") return false;
989
+ if (entry.variant?.keyframes) return false;
990
+ return additiveChannels(channelsFor(entry));
991
+ }
992
+ function resolveComposition(entries, registry, warnings) {
993
+ if (entries.length <= 1) return entries;
994
+ const claims = entries.map((e) => ({
995
+ name: e.spec.name,
996
+ channels: channelsFor(e),
997
+ gate: e.spec.gate,
998
+ phase: phaseOf(e),
999
+ additive: additivelyComposable(e),
1000
+ delivery: e.resolved.preset.delivery,
1001
+ // "Writes `animation-name` inline" needs no declaration of its own: `buildPlan` sends exactly
1002
+ // the `css-keyframes` entries to `pushTrack`, and `declarations.ts` emits the longhands from
1003
+ // the tracks. Only the losing side of a delivery clobber has to say what it is.
1004
+ inlineAnimation: e.resolved.primitive.renderer === "css-keyframes"
1005
+ }));
1006
+ const conflicts = findConflicts(claims);
1007
+ const clobbers = deliveryClobbers(claims);
1008
+ if (conflicts.length === 0 && clobbers.length === 0) return entries;
1009
+ if (clobbers.length === 0) {
1010
+ const additive = additiveResolution(claims, conflicts);
1011
+ if (additive) return entries.map((e, i) => additive.has(i) ? { ...e, composite: "add" } : e);
1012
+ }
1013
+ const combo = registry.findCombo(entries.map((e) => e.spec.name));
1014
+ const remedy = combo ? `Use the "${combo.preset.name}" effect instead.` : "Apply them to nested elements, or register a combined effect.";
1015
+ const kept = entries[0].spec.name;
1016
+ const dropped = entries.slice(1).map((entry) => `"${entry.spec.name}"`);
1017
+ const loss = `Dropped ${dropped.join(", ")} \u2014 only "${kept}" will run.`;
1018
+ const diagnosis = [...conflicts.length > 0 ? [describeConflicts(conflicts)] : [], ...clobbers];
1019
+ warnings.push(`cannot compose: ${diagnosis.join("; ")}. ${loss} ${remedy}`);
1020
+ return [entries[0]];
1021
+ }
1022
+ function buildPlan(entries, timeline, unknown, warnings) {
1023
+ const plan = emptyPlan(unknown, warnings);
1024
+ const tracks = emptyTracks();
1025
+ const channels = /* @__PURE__ */ new Set();
1026
+ const transitionSegments = [];
1027
+ const transitionOwners = /* @__PURE__ */ new Map();
1028
+ let activations;
1029
+ let timelines;
1030
+ for (const entry of entries) {
1031
+ const { preset, primitive } = entry.resolved;
1032
+ const step = entry.step;
1033
+ plan.fxNames.push(preset.name);
1034
+ plan.reducedMotion = strictestPolicy(plan.reducedMotion, primitive.reducedMotion);
1035
+ activations = intersect(activations, primitive.supportedActivations);
1036
+ timelines = intersect(timelines, primitive.supportedTimelines);
1037
+ for (const channel of channelsFor(entry)) channels.add(channel);
1038
+ warnUnsupportedTimeline(preset.name, primitive.supportedTimelines, timeline, warnings);
1039
+ Object.assign(
1040
+ plan.vars,
1041
+ resolveParams(authoredParams(entry), schemaFor(entry), (m) => warnings.push(m))
1042
+ );
1043
+ if (primitive.renderer === "css-keyframes") pushTrack(tracks, entry, step);
1044
+ else plan.jsEffects.push(positioned(entry, step));
1045
+ pushTransitions(transitionSegments, transitionOwners, entry, warnings);
1046
+ }
1047
+ Object.assign(plan.declarations, declarationsFor(tracks, timeline));
1048
+ plan.keyframeNames = tracks.keyframes;
1049
+ if (transitionSegments.length > 0) plan.transition = transitionSegments.join(", ");
1050
+ plan.supportedActivations = activations;
1051
+ plan.supportedTimelines = timelines;
1052
+ plan.channels = [...channels];
1053
+ return plan;
1054
+ }
1055
+ function memberFor(entry) {
1056
+ const { spec, resolved } = entry;
1057
+ const { preset, primitive } = resolved;
1058
+ const authored = authoredParams(entry);
1059
+ return {
1060
+ name: preset.name,
1061
+ primitiveId: primitive.id,
1062
+ at: spec.at,
1063
+ delay: spec.delay,
1064
+ duration: spec.duration,
1065
+ cascadeDelay: cascadeValue(authored, preset, primitive.parameters, "delay"),
1066
+ cascadeDuration: cascadeValue(authored, preset, primitive.parameters, "duration"),
1067
+ repeat: spec.repeat,
1068
+ // A `css-keyframes` segment is always positionable — it compiles to an `animation-delay` and
1069
+ // the browser honours it. A JavaScript-rendered one is positionable only if it declares the
1070
+ // parameter, which is the single compile-time signal that it reads a delay at all.
1071
+ positionable: primitive.renderer === "css-keyframes" || Object.hasOwn(primitive.parameters, "delay")
1072
+ };
1073
+ }
1074
+ function cascadeValue(authored, preset, schema, name) {
1075
+ const candidates = [authored[name], preset.params?.[name], schema[name]?.default];
1076
+ return candidates.find((value) => value !== void 0 && isReadableTime(value));
1077
+ }
1078
+ function positioned(entry, step) {
1079
+ if (!step.sequenced) return entry;
1080
+ return { ...entry, sequencedDelayMs: step.delayMs };
1081
+ }
1082
+ function warnUnsupportedTimeline(name, supported, timeline, warnings) {
1083
+ if (supported.includes(timeline)) return;
1084
+ warnings.push(
1085
+ `"${name}" does not support timeline "${timeline}" (supports: ${supported.join(", ")})`
1086
+ );
1087
+ }
1088
+
1089
+ // src/core/derived/book.ts
1090
+ function createDerivedBook() {
1091
+ return {
1092
+ derived: /* @__PURE__ */ new WeakMap(),
1093
+ hosts: /* @__PURE__ */ new WeakMap(),
1094
+ groups: /* @__PURE__ */ new WeakMap(),
1095
+ contexts: /* @__PURE__ */ new WeakMap(),
1096
+ unions: /* @__PURE__ */ new WeakMap(),
1097
+ cleanups: /* @__PURE__ */ new WeakMap()
1098
+ };
1099
+ }
1100
+ function derivedOf(book, el) {
1101
+ return [...book.derived.get(el) ?? []];
1102
+ }
1103
+ function hostOf(book, el) {
1104
+ return book.hosts.get(el);
1105
+ }
1106
+
1107
+ // src/core/derived/aggregate.ts
1108
+ function isForwardRunning(state) {
1109
+ return state.status === "running" && state.direction !== "reverse";
1110
+ }
1111
+ function isRunning(state) {
1112
+ return state.status === "running";
1113
+ }
1114
+ function isFailed(state) {
1115
+ return state.status === "failed";
1116
+ }
1117
+ function isSettled(state) {
1118
+ return state.status === "finished" || state.status === "failed";
1119
+ }
1120
+ function isReady(state) {
1121
+ return state.status === "ready";
1122
+ }
1123
+ function matchStates(port, host) {
1124
+ const states = [];
1125
+ for (const match of derivedOf(port.book, host)) {
1126
+ const state = port.stateOf(match);
1127
+ if (state) states.push(state);
1128
+ }
1129
+ return states;
1130
+ }
1131
+ function applyRunning(port, host, hostState, states) {
1132
+ if (hostState.status === "running") return;
1133
+ port.writeStatus(host, hostState, "running");
1134
+ if (states.some(isForwardRunning)) port.emit(host, hostState, KUI_EVENT.start, "activated");
1135
+ }
1136
+ function applyFinished(port, host, hostState, states) {
1137
+ if (hostState.status === "finished") return;
1138
+ const wasRunning = hostState.status === "running";
1139
+ port.writeStatus(host, hostState, "finished");
1140
+ if (states.every((state) => state.cancelled === true)) return;
1141
+ port.emit(host, hostState, KUI_EVENT.finish, wasRunning ? "complete" : "reduced-motion");
1142
+ }
1143
+ function syncAggregate(port, host) {
1144
+ const hostState = port.stateOf(host);
1145
+ if (!hostState || !hostState.aggregate) return;
1146
+ const states = matchStates(port, host);
1147
+ if (states.length === 0) return;
1148
+ if (states.some(isRunning)) {
1149
+ applyRunning(port, host, hostState, states);
1150
+ } else if (states.every(isFailed)) {
1151
+ port.writeStatus(host, hostState, "failed");
1152
+ } else if (states.every(isSettled)) {
1153
+ applyFinished(port, host, hostState, states);
1154
+ } else if (states.every(isReady)) {
1155
+ port.writeStatus(host, hostState, "ready");
1156
+ }
1157
+ }
1158
+ function unionKey(targets) {
1159
+ return targets.map((target) => `${target.scope} ${target.selector}`).sort((a, b) => a.localeCompare(b)).join("");
1160
+ }
1161
+ function compileUnion(port, context, targets) {
1162
+ const key = unionKey(targets);
1163
+ const cache = port.book.unions.get(context.host);
1164
+ const cached2 = cache?.get(key);
1165
+ if (cached2) return cached2;
1166
+ const first = targets[0];
1167
+ const compiled = compileTargets(
1168
+ { specs: targets.flatMap((target) => target.specs), warnings: [] },
1169
+ port.registry,
1170
+ context.timeline
1171
+ );
1172
+ const union = compiled.targets[0];
1173
+ union.plan.reducedMotion = first.plan.reducedMotion;
1174
+ const win = context.host.ownerDocument?.defaultView ?? void 0;
1175
+ union.plan.jsEffects = union.plan.jsEffects.filter((entry) => gateMatches(entry.spec.gate, win));
1176
+ const known = new Set(targets.flatMap((target) => target.plan.warnings));
1177
+ for (const warning of union.plan.warnings) {
1178
+ if (!known.has(warning)) port.reporter.warn(warning, context.host);
1179
+ }
1180
+ const result = { ...union, selector: first.selector, scope: first.scope };
1181
+ const unions = cache ?? /* @__PURE__ */ new Map();
1182
+ unions.set(key, result);
1183
+ if (!cache) port.book.unions.set(context.host, unions);
1184
+ return result;
1185
+ }
1186
+
1187
+ // src/core/style-plan.ts
1188
+ function planStyles(input) {
1189
+ const { plan, config, capabilities, respectReducedMotion } = input;
1190
+ const reduce = respectReducedMotion && capabilities.reducedMotion;
1191
+ const { scrubbed, useNativeTimeline } = timelineMode(plan, config, capabilities);
1192
+ const properties = { ...plan.vars, ...plan.declarations };
1193
+ Object.assign(properties, transitionProperty(plan));
1194
+ Object.assign(properties, timelineProperties(config, capabilities, useNativeTimeline));
1195
+ const hasCssAnimation = Object.keys(plan.declarations).length > 0;
1196
+ const elementHasCssAnimation = input.elementHasCssAnimation ?? hasCssAnimation;
1197
+ const gate = resolveGate({
1198
+ useNativeTimeline,
1199
+ scrubbed: scrubbed && elementHasCssAnimation,
1200
+ reduce,
1201
+ activation: config.activation,
1202
+ // JS effects are gated too. They emit no `animation` declaration, so only the play-state
1203
+ // write is skipped — the activation itself still has to be bound, or `on:enter` and
1204
+ // `on:click` would silently do nothing for every pinned, dragged, or morphing element.
1205
+ hasWork: elementHasCssAnimation || plan.jsEffects.length > 0,
1206
+ hasCssAnimation,
1207
+ // A browser lacking standalone translate/rotate/scale support silently ignores any
1208
+ // `@keyframes` step written in those properties — the animation never visibly completes. An
1209
+ // effect deferred on that promise would sit paused (or, for an entrance reveal, invisible)
1210
+ // forever, so it must reach its final state immediately instead, the same fail-open rule
1211
+ // already applied under reduced motion.
1212
+ unsupportedTransform: needsIndividualTransforms(plan.channels, capabilities)
1213
+ });
1214
+ if (gate === "deferred" || gate === "scrubbed") properties["animation-play-state"] = "paused";
1215
+ return {
1216
+ properties,
1217
+ attributes: {
1218
+ [ATTR.normalized]: plan.fxNames.join(" "),
1219
+ [ATTR.rm]: plan.reducedMotion,
1220
+ [ATTR.state]: "ready"
1221
+ },
1222
+ gate,
1223
+ activation: gate === "deferred" ? effectiveActivation(config) : null
1224
+ };
1225
+ }
1226
+ function timelineMode(plan, config, capabilities) {
1227
+ const scrubbed = config.timeline === "pin" && plan.supportedTimelines.includes("pin");
1228
+ const useNativeTimeline = !scrubbed && supportsTimeline(config.timeline, capabilities) && plan.supportedTimelines.includes(config.timeline);
1229
+ return { scrubbed, useNativeTimeline };
1230
+ }
1231
+ function transitionProperty(plan) {
1232
+ return plan.transition ? { "--kui-transition": plan.transition } : {};
1233
+ }
1234
+ function timelineProperties(config, capabilities, useNativeTimeline) {
1235
+ if (!useNativeTimeline) return {};
1236
+ const properties = {
1237
+ "animation-timeline": config.timeline === "scroll" ? "scroll()" : "view()"
1238
+ };
1239
+ if (capabilities.animationRange) {
1240
+ properties["animation-range"] = config.range || (config.timeline === "scroll" ? "0% 100%" : "entry 0% cover 60%");
1241
+ }
1242
+ return properties;
1243
+ }
1244
+ function resolveGate(input) {
1245
+ if (input.useNativeTimeline) return "native-timeline";
1246
+ if (input.scrubbed) return "scrubbed";
1247
+ if (!input.hasWork) return "immediate";
1248
+ if (input.reduce || startKindOf(input.activation) === "immediate" || input.unsupportedTransform) {
1249
+ return "immediate";
1250
+ }
1251
+ return "deferred";
1252
+ }
1253
+ function needsIndividualTransforms(channels, capabilities) {
1254
+ if (capabilities.individualTransforms) return false;
1255
+ return channels.some((c) => c === "translate" || c === "rotate" || c === "scale");
1256
+ }
1257
+ function supportsTimeline(timeline, capabilities) {
1258
+ if (timeline === "view") return capabilities.viewTimeline;
1259
+ if (timeline === "scroll") return capabilities.scrollTimeline;
1260
+ return false;
1261
+ }
1262
+ function effectiveActivation(config) {
1263
+ const observed = "enter";
1264
+ if (config.timeline !== "time" && startKindOf(config.activation) === "manual") return observed;
1265
+ return config.activation;
1266
+ }
1267
+
1268
+ // src/core/stagger-keys.ts
1269
+ var GROUP_KEY_NAMES = ["cascade", "spread", "order", "cols", "along"];
1270
+ function collectKeys(lookup) {
1271
+ const keys = {};
1272
+ for (const name of GROUP_KEY_NAMES) {
1273
+ const value = lookup(name);
1274
+ if (value !== void 0) keys[name] = value;
1275
+ }
1276
+ return Object.keys(keys).length > 0 ? keys : void 0;
1277
+ }
1278
+ function elementStaggerKeys(parsed) {
1279
+ return collectKeys((name) => parsed[name]);
1280
+ }
1281
+ function staggerKeysFor(scoped, elementWide) {
1282
+ return collectKeys((name) => scoped?.[name] ?? elementWide?.[name]);
1283
+ }
1284
+ function isGroupedKeys(keys) {
1285
+ return keys?.cascade !== void 0 || keys?.spread !== void 0 || keys?.order !== void 0;
1286
+ }
1287
+ var TARGET_GROUP_HOSTS = /* @__PURE__ */ new WeakMap();
1288
+ function claimTargetGroupHost(host) {
1289
+ TARGET_GROUP_HOSTS.set(host, (TARGET_GROUP_HOSTS.get(host) ?? 0) + 1);
1290
+ let released = false;
1291
+ return () => {
1292
+ if (released) return;
1293
+ released = true;
1294
+ const remaining = TARGET_GROUP_HOSTS.get(host) - 1;
1295
+ if (remaining > 0) TARGET_GROUP_HOSTS.set(host, remaining);
1296
+ else TARGET_GROUP_HOSTS.delete(host);
1297
+ };
1298
+ }
1299
+ function isTargetGroupHost(el) {
1300
+ return TARGET_GROUP_HOSTS.has(el);
1301
+ }
1302
+
1303
+ // src/core/derived/group-gate.ts
1304
+ function groupGateOwner(port, context, target) {
1305
+ const keys = staggerKeysFor(target.hoists, elementStaggerKeys(context.parsed));
1306
+ return isGroupedKeys(keys) ? context.host : void 0;
1307
+ }
1308
+ function skipsOwnBinding(state) {
1309
+ return state.gateOwner !== void 0;
1310
+ }
1311
+ function activateGroup(port, group, oneShot) {
1312
+ for (const member of group.members) {
1313
+ if (oneShot && port.stateOf(member)?.status !== "ready") continue;
1314
+ port.activateOne(member);
1315
+ }
1316
+ }
1317
+ function deactivateGroup(port, group) {
1318
+ for (const member of group.members) port.deactivateOne(member);
1319
+ }
1320
+ function registerCleanup(port, host, cleanup) {
1321
+ const cleanups = port.book.cleanups.get(host) ?? [];
1322
+ cleanups.push(cleanup);
1323
+ port.book.cleanups.set(host, cleanups);
1324
+ }
1325
+ function bindOneGroup(port, host, group) {
1326
+ const stylePlan = planStyles({
1327
+ plan: group.target.plan,
1328
+ config: group.config,
1329
+ capabilities: port.capabilities,
1330
+ respectReducedMotion: port.respectReducedMotion
1331
+ });
1332
+ if (stylePlan.gate !== "deferred") return;
1333
+ const activation = stylePlan.activation;
1334
+ const oneShot = isOneShot(resolveActivationSpec(activation));
1335
+ const release = port.binder.bind(host, activation, {
1336
+ threshold: group.config.threshold,
1337
+ from: group.config.activationSource,
1338
+ activate: () => activateGroup(port, group, oneShot),
1339
+ deactivate: () => deactivateGroup(port, group)
1340
+ });
1341
+ registerCleanup(port, host, release);
1342
+ }
1343
+ function bindGroups(port, host) {
1344
+ const groups = port.book.groups.get(host);
1345
+ if (groups === void 0) return;
1346
+ for (const group of groups) {
1347
+ if (group.gateOwner === host) bindOneGroup(port, host, group);
1348
+ }
1349
+ }
1350
+
1351
+ // src/core/stagger-config.ts
1352
+ var FROM_KEYWORDS = /* @__PURE__ */ new Set(["start", "end", "center", "edges", "random"]);
1353
+ var ORDER_KEYS = /* @__PURE__ */ new Set(["from", "order"]);
1354
+ var SPREAD_KEY = "spread";
1355
+ var COLS_KEY = "cols";
1356
+ var AXIS_KEYS = /* @__PURE__ */ new Set(["axis", "along"]);
1357
+ var AXIS_VALUES = /* @__PURE__ */ new Set(["x", "y"]);
1358
+ var ORIGIN_RE = /^(\d+(?:\.\d+)?|\.\d+)\/(\d+(?:\.\d+)?|\.\d+)$/;
1359
+ var COLS_RE = /^\d{1,9}$/;
1360
+ var PAIR_RE = /^([a-zA-Z-]+):(.*)$/;
1361
+ var INDEX_RE = /^-?\d{1,9}$/;
1362
+ function parseStaggerTokens(value, warnings) {
1363
+ const config = { from: "start" };
1364
+ const seen = { from: false, spread: false, cols: false, along: false };
1365
+ for (const token of splitTopLevel(value, " ", warnings)) {
1366
+ const pair = PAIR_RE.exec(token);
1367
+ if (pair) applyStaggerPair(pair[1], pair[2], { config, seen, warnings });
1368
+ else config.step = keepFirstStep(config.step, token, warnings);
1369
+ }
1370
+ return { config, sawFrom: seen.from };
1371
+ }
1372
+ function applyStaggerPair(key, raw, state) {
1373
+ const slot = slotFor(key);
1374
+ if (slot === void 0) {
1375
+ state.warnings.push(
1376
+ `unrecognised key "${key}" in data-kui-stagger \u2014 expected a time step, "spread:", "cols:", "along:", "from:" or "order:"`
1377
+ );
1378
+ return;
1379
+ }
1380
+ if (claim(slot, raw, state, key)) ASSIGN[slot](raw, state, key);
1381
+ }
1382
+ function slotFor(key) {
1383
+ if (key === SPREAD_KEY) return "spread";
1384
+ if (key === COLS_KEY) return "cols";
1385
+ if (AXIS_KEYS.has(key)) return "along";
1386
+ if (ORDER_KEYS.has(key)) return "from";
1387
+ return void 0;
1388
+ }
1389
+ var ASSIGN = {
1390
+ spread: (raw, state) => {
1391
+ state.config.spread = raw.trim();
1392
+ },
1393
+ cols: (raw, state) => {
1394
+ state.config.cols = parseCols(raw, state.warnings);
1395
+ },
1396
+ along: (raw, state, key) => {
1397
+ state.config.along = parseAxis(raw, state.warnings, key);
1398
+ },
1399
+ from: (raw, state, key) => {
1400
+ state.config.from = parseFrom(raw, state.warnings, key);
1401
+ }
1402
+ };
1403
+ function claim(slot, raw, state, spelling) {
1404
+ if (state.seen[slot]) {
1405
+ state.warnings.push(`duplicate "${spelling}:" in data-kui-stagger \u2014 "${raw}" ignored`);
1406
+ return false;
1407
+ }
1408
+ state.seen[slot] = true;
1409
+ return true;
1410
+ }
1411
+ function parseCols(raw, warnings) {
1412
+ const value = raw.trim();
1413
+ if (value === "auto") return "auto";
1414
+ if (COLS_RE.test(value) && Number(value) > 0) return Number(value);
1415
+ warnings.push(`unrecognised "cols:${raw}" \u2014 expected a column count or "auto"`);
1416
+ return void 0;
1417
+ }
1418
+ function parseAxis(raw, warnings, key) {
1419
+ const value = raw.trim();
1420
+ if (AXIS_VALUES.has(value)) return value;
1421
+ warnings.push(`unrecognised "${key}:${raw}" \u2014 expected x or y`);
1422
+ return void 0;
1423
+ }
1424
+ function resolveStaggerConfig(attribute, source, warnings = []) {
1425
+ const inline = inlineGroupKeys(source);
1426
+ if (attribute === null && inline === void 0) return void 0;
1427
+ return resolveStaggerConfigFrom(attribute, inline, warnings);
1428
+ }
1429
+ function resolveStaggerConfigFrom(attribute, inline, warnings = []) {
1430
+ if (attribute === null && inline === void 0) return void 0;
1431
+ const { config: longhand, sawFrom } = parseStaggerTokens(attribute ?? "", warnings);
1432
+ return oneStepMode(screenStep(mergeInline(longhand, sawFrom, inline ?? {}, warnings), warnings), warnings);
1433
+ }
1434
+ function oneStepMode(config, warnings) {
1435
+ if (config.spread === void 0 || config.step === void 0) return config;
1436
+ warnings.push(
1437
+ `stagger step "${config.step}" is ignored \u2014 "spread:${config.spread}" already budgets the whole group, and the step is what it divides out`
1438
+ );
1439
+ const budgeted = { ...config };
1440
+ delete budgeted.step;
1441
+ return budgeted;
1442
+ }
1443
+ function inlineGroupKeys(source) {
1444
+ if (!hasGroupKey(source)) return void 0;
1445
+ const { cascade, spread, order, cols, along } = parse(source);
1446
+ const keys = { cascade, spread, order, cols, along };
1447
+ return Object.values(keys).some((value) => value !== void 0) ? keys : void 0;
1448
+ }
1449
+ function screenStep(config, warnings) {
1450
+ const screened = { ...config };
1451
+ if (!keepValue("step", config.step, warnings)) delete screened.step;
1452
+ if (!keepValue("spread", config.spread, warnings)) delete screened.spread;
1453
+ return screened;
1454
+ }
1455
+ function keepValue(label, value, warnings) {
1456
+ if (value === void 0) return false;
1457
+ if (isSafeCssValue(value)) return true;
1458
+ warnings.push(`stagger ${label} "${value}" contains disallowed CSS syntax \u2014 ignored`);
1459
+ return false;
1460
+ }
1461
+ function mergeInline(longhand, sawFrom, inline, warnings) {
1462
+ const config = { ...longhand };
1463
+ if (inline.cascade !== void 0) {
1464
+ warnOverride("stagger step", longhand.step, inline.cascade, warnings);
1465
+ config.step = inline.cascade;
1466
+ }
1467
+ if (inline.spread !== void 0) {
1468
+ warnOverride("stagger budget", longhand.spread, inline.spread, warnings);
1469
+ config.spread = inline.spread;
1470
+ }
1471
+ mergeInlineGrid(config, longhand, inline, warnings);
1472
+ if (inline.order !== void 0) {
1473
+ const order = parseFrom(inline.order, warnings, "order");
1474
+ warnOverride("stagger order", sawFrom ? longhand.from : void 0, order, warnings);
1475
+ config.from = order;
1476
+ }
1477
+ return config;
1478
+ }
1479
+ function mergeInlineGrid(config, longhand, inline, warnings) {
1480
+ if (inline.cols !== void 0) {
1481
+ const cols = parseCols(inline.cols, warnings);
1482
+ warnOverride("stagger columns", longhand.cols, cols, warnings);
1483
+ if (cols !== void 0) config.cols = cols;
1484
+ }
1485
+ if (inline.along !== void 0) {
1486
+ const along = parseAxis(inline.along, warnings, "along");
1487
+ warnOverride("stagger axis", longhand.along, along, warnings);
1488
+ if (along !== void 0) config.along = along;
1489
+ }
1490
+ }
1491
+ function hasGroupKey(source) {
1492
+ return GROUP_KEY_PREFIXES.some((prefix) => source.includes(prefix));
1493
+ }
1494
+ var GROUP_KEY_PREFIXES = [
1495
+ "cascade:",
1496
+ "spread:",
1497
+ "order:",
1498
+ "cols:",
1499
+ "along:"
1500
+ ];
1501
+ function warnOverride(label, previous, next, warnings) {
1502
+ if (previous === void 0 || next === void 0 || previous === next) return;
1503
+ warnings.push(
1504
+ `conflicting ${label}: data-kui-stagger says "${String(previous)}", data-kui says "${String(next)}" \u2014 data-kui wins`
1505
+ );
1506
+ }
1507
+ function keepFirstStep(current, token, warnings) {
1508
+ if (current === void 0) return token;
1509
+ warnings.push(`extra token "${token}" in data-kui-stagger (expected one time step)`);
1510
+ return current;
1511
+ }
1512
+ function parseFrom(raw, warnings, key = "from") {
1513
+ const value = raw.trim();
1514
+ if (FROM_KEYWORDS.has(value)) return value;
1515
+ if (INDEX_RE.test(value)) return Number(value);
1516
+ const origin = ORIGIN_RE.exec(value);
1517
+ if (origin) return { x: clamp01(Number(origin[1]), value, warnings), y: clamp01(Number(origin[2]), value, warnings) };
1518
+ warnings.push(
1519
+ `unrecognised "${key}:${raw}" \u2014 expected start, end, center, edges, random, a child index, or an "x/y" point in a grid`
1520
+ );
1521
+ return "start";
1522
+ }
1523
+ function clamp01(value, source, warnings) {
1524
+ if (value >= 0 && value <= 1) return value;
1525
+ warnings.push(`stagger order "${source}" is outside the grid (0 to 1 on each axis) \u2014 clamped`);
1526
+ return Math.min(Math.max(value, 0), 1);
1527
+ }
1528
+
1529
+ // src/core/stagger.ts
1530
+ var RANK_PRECISION = 1e3;
1531
+ var RANDOM_SALT = 2654435761;
1532
+ var COUNT_SALT = 2246822507;
1533
+ var MIX_A = 569420461;
1534
+ var MIX_B = 1935289751;
1535
+ function resolveStep(config, maxRank) {
1536
+ if (config.spread === void 0) return config.step;
1537
+ if (maxRank <= 0) return "0ms";
1538
+ return `calc((${config.spread}) / ${String(maxRank)})`;
1539
+ }
1540
+ function staggerRanks(count, from, warnings = [], layout) {
1541
+ if (count <= 0) return [];
1542
+ if (from === "random") return randomRanks(count);
1543
+ if (layout) return gridRanks(count, from, layout, warnings);
1544
+ if (typeof from === "object") {
1545
+ warnings.push(
1546
+ `stagger order "${String(from.x)}/${String(from.y)}" is a point in a grid, but this group has no "cols:" \u2014 add one, or name an edge with start/end/center/edges`
1547
+ );
1548
+ return staggerRanks(count, "start", warnings);
1549
+ }
1550
+ const last = count - 1;
1551
+ if (from === "edges") {
1552
+ return Array.from({ length: count }, (_, index) => Math.min(index, last - index));
1553
+ }
1554
+ const origin = originOf(from, last, warnings);
1555
+ return Array.from({ length: count }, (_, index) => Math.floor(Math.abs(index - origin)));
1556
+ }
1557
+ function originOf(from, last, warnings) {
1558
+ if (from === "start") return 0;
1559
+ if (from === "end") return last;
1560
+ if (from === "center") return last / 2;
1561
+ return clampIndex(from, last, warnings);
1562
+ }
1563
+ function clampIndex(index, last, warnings) {
1564
+ if (index < 0 || index > last) {
1565
+ warnings.push(`stagger order "${String(index)}" is outside the group (0 to ${String(last)}) \u2014 clamped`);
1566
+ }
1567
+ return Math.min(Math.max(index, 0), last);
1568
+ }
1569
+ function gridRanks(count, from, layout, warnings) {
1570
+ const cols = Math.min(Math.max(1, layout.cols), count);
1571
+ const rows = Math.ceil(count / cols);
1572
+ if (from === "edges") {
1573
+ const inward = gridRanks(count, "center", layout, warnings);
1574
+ let furthest = 0;
1575
+ for (const rank of inward) furthest = Math.max(furthest, rank);
1576
+ return inward.map((rank) => round(furthest - rank));
1577
+ }
1578
+ const origin = gridOrigin(from, { cols, rows, last: count - 1 }, warnings);
1579
+ return Array.from({ length: count }, (_, index) => {
1580
+ const dx = index % cols - origin.x;
1581
+ const dy = Math.floor(index / cols) - origin.y;
1582
+ if (layout.along === "x") return round(Math.abs(dx));
1583
+ if (layout.along === "y") return round(Math.abs(dy));
1584
+ return round(Math.hypot(dx, dy));
1585
+ });
1586
+ }
1587
+ function gridOrigin(from, grid, warnings) {
1588
+ const { cols, rows, last } = grid;
1589
+ if (typeof from === "object") return { x: from.x * (cols - 1), y: from.y * (rows - 1) };
1590
+ if (from === "start") return { x: 0, y: 0 };
1591
+ if (from === "end") return { x: cols - 1, y: rows - 1 };
1592
+ if (from === "center") return { x: (cols - 1) / 2, y: (rows - 1) / 2 };
1593
+ const index = clampIndex(from, last, warnings);
1594
+ return { x: index % cols, y: Math.floor(index / cols) };
1595
+ }
1596
+ function round(value) {
1597
+ return Math.round(value * RANK_PRECISION) / RANK_PRECISION;
1598
+ }
1599
+ function randomRanks(count) {
1600
+ const order = Array.from({ length: count }, (_, index) => index);
1601
+ order.sort((a, b) => scatterKey(a, count) - scatterKey(b, count));
1602
+ const ranks = new Array(count);
1603
+ for (const [rank, index] of order.entries()) ranks[index] = rank;
1604
+ return ranks;
1605
+ }
1606
+ function scatterKey(index, count) {
1607
+ let hash = (Math.imul(index, RANDOM_SALT) ^ Math.imul(count, COUNT_SALT)) >>> 0;
1608
+ hash ^= hash >>> 16;
1609
+ hash = Math.imul(hash, MIX_A) >>> 0;
1610
+ hash ^= hash >>> 15;
1611
+ hash = Math.imul(hash, MIX_B) >>> 0;
1612
+ hash ^= hash >>> 15;
1613
+ return hash >>> 0;
1614
+ }
1615
+ function indexStaggerGroup(group, reporter) {
1616
+ const warnings = [];
1617
+ const ledgers = reopenLedger(group);
1618
+ const config = resolveStaggerConfig(
1619
+ group.getAttribute(ATTR.stagger),
1620
+ group.getAttribute(ATTR.source) ?? "",
1621
+ warnings
1622
+ ) ?? { from: "start" };
1623
+ const children = animatedChildren(group);
1624
+ const ranks = staggerRanks(children.length, config.from, warnings, resolveLayout(config, children, warnings));
1625
+ let maxRank = 0;
1626
+ for (const [index, child] of children.entries()) {
1627
+ const rank = ranks[index];
1628
+ ledgers.style(child).set("--kui-i", String(rank));
1629
+ GROUP_OF_CHILD.set(child, group);
1630
+ if (rank > maxRank) maxRank = rank;
1631
+ }
1632
+ const step = resolveStep(config, maxRank);
1633
+ if (step) ledgers.style(group).set("--kui-stagger", step);
1634
+ ledgers.style(group).set("--kui-stagger-count", String(round(maxRank + 1)));
1635
+ for (const warning of warnings) reporter?.warn(warning, group);
1636
+ }
1637
+ var GROUP_LEDGERS = /* @__PURE__ */ new WeakMap();
1638
+ var GROUP_OF_CHILD = /* @__PURE__ */ new WeakMap();
1639
+ function restageAfterRemoval(removed, reporter) {
1640
+ const groups = /* @__PURE__ */ new Set();
1641
+ for (const el of removed) {
1642
+ const group = GROUP_OF_CHILD.get(el);
1643
+ if (group) groups.add(group);
1644
+ }
1645
+ for (const group of groups) indexStaggerGroup(group, reporter);
1646
+ }
1647
+ function reopenLedger(group) {
1648
+ GROUP_LEDGERS.get(group)?.restore();
1649
+ const ledgers = createLedgerSet(group);
1650
+ GROUP_LEDGERS.set(group, ledgers);
1651
+ return ledgers;
1652
+ }
1653
+ function releaseStaggerGroup(group) {
1654
+ GROUP_LEDGERS.get(group)?.restore();
1655
+ GROUP_LEDGERS.delete(group);
1656
+ }
1657
+ function releaseStagger(root) {
1658
+ const selector = `[${ATTR.stagger}], [${ATTR.source}]`;
1659
+ if (root instanceof Element && root.matches(selector)) releaseStaggerGroup(root);
1660
+ for (const el of root.querySelectorAll(selector)) releaseStaggerGroup(el);
1661
+ }
1662
+ function restageAround(el, reporter) {
1663
+ restageOne(el, reporter);
1664
+ const parent = el.parentElement;
1665
+ if (parent) restageOne(parent, reporter);
1666
+ }
1667
+ function restageOne(el, reporter) {
1668
+ if (declaresGroup(el)) indexStaggerGroup(el, reporter);
1669
+ else releaseStaggerGroup(el);
1670
+ }
1671
+ function indexTargetGroup(request) {
1672
+ const { host, matches, styleOf, keys, reporter } = request;
1673
+ const warnings = [];
1674
+ const attribute = host.getAttribute(ATTR.stagger);
1675
+ const config = (keys ? resolveStaggerConfigFrom(attribute, keys, warnings) : resolveStaggerConfig(attribute, host.getAttribute(ATTR.source) ?? "", warnings)) ?? { from: "start" };
1676
+ const maxRank = rankBuckets(bucketByParent(matches), config, styleOf, warnings);
1677
+ const step = resolveStep(config, maxRank);
1678
+ if (step) styleOf(host).set("--kui-stagger", step);
1679
+ styleOf(host).set("--kui-stagger-count", String(round(maxRank + 1)));
1680
+ for (const warning of warnings) reporter?.warn(warning, host);
1681
+ }
1682
+ function bucketByParent(matches) {
1683
+ const byParent = /* @__PURE__ */ new Map();
1684
+ for (const match of matches) {
1685
+ const siblings = byParent.get(match.parentElement);
1686
+ if (siblings) siblings.push(match);
1687
+ else byParent.set(match.parentElement, [match]);
1688
+ }
1689
+ return byParent;
1690
+ }
1691
+ function rankBuckets(byParent, config, styleOf, warnings) {
1692
+ let maxRank = 0;
1693
+ for (const siblings of byParent.values()) {
1694
+ const layout = resolveLayout(config, siblings, warnings);
1695
+ const ranks = staggerRanks(siblings.length, config.from, warnings, layout);
1696
+ for (const [index, match] of siblings.entries()) {
1697
+ const rank = ranks[index];
1698
+ styleOf(match).set("--kui-i", String(rank));
1699
+ if (rank > maxRank) maxRank = rank;
1700
+ }
1701
+ }
1702
+ return maxRank;
1703
+ }
1704
+ function resolveLayout(config, members, warnings) {
1705
+ if (config.cols === void 0) {
1706
+ if (config.along !== void 0) {
1707
+ warnings.push(
1708
+ `"along:${config.along}" names an axis of a grid this group has not declared \u2014 add "cols:" (a column count, or "auto")`
1709
+ );
1710
+ }
1711
+ return void 0;
1712
+ }
1713
+ const cols = config.cols === "auto" ? measureColumns(members, warnings) : config.cols;
1714
+ if (cols === void 0) return void 0;
1715
+ return config.along === void 0 ? { cols } : { cols, along: config.along };
1716
+ }
1717
+ function measureColumns(members, warnings) {
1718
+ if (members.length < 2) return 1;
1719
+ const lefts = [];
1720
+ let laidOut = false;
1721
+ for (const member of members) {
1722
+ const box = member.getBoundingClientRect();
1723
+ if (box.width !== 0 || box.height !== 0) laidOut = true;
1724
+ lefts.push(box.left);
1725
+ }
1726
+ if (!laidOut) {
1727
+ warnings.push(
1728
+ `"cols:auto" could not measure this group \u2014 none of its children have been laid out yet (display:none, or detached), so the stagger falls back to DOM order`
1729
+ );
1730
+ return void 0;
1731
+ }
1732
+ const direction = Math.sign(lefts[1] - lefts[0]);
1733
+ if (direction === 0) return 1;
1734
+ let cols = 1;
1735
+ for (let index = 1; index < lefts.length; index++) {
1736
+ if (Math.sign(lefts[index] - lefts[index - 1]) !== direction) break;
1737
+ cols = index + 1;
1738
+ }
1739
+ return cols;
1740
+ }
1741
+ function animatedChildren(group) {
1742
+ const children = [];
1743
+ for (const child of group.children) {
1744
+ if (child.hasAttribute(ATTR.source)) children.push(child);
1745
+ }
1746
+ return children;
1747
+ }
1748
+ function applyStagger(root, reporter) {
1749
+ const selector = `[${ATTR.stagger}], [${ATTR.source}]`;
1750
+ if (root instanceof Element && root.matches(selector)) restageOne(root, reporter);
1751
+ for (const group of root.querySelectorAll(selector)) restageOne(group, reporter);
1752
+ }
1753
+ function declaresGroup(el) {
1754
+ if (el.hasAttribute(ATTR.define)) return false;
1755
+ if (isTargetGroupHost(el)) return false;
1756
+ if (el.hasAttribute(ATTR.stagger)) return true;
1757
+ return resolveStaggerConfig(null, el.getAttribute(ATTR.source) ?? "") !== void 0;
1758
+ }
1759
+
1760
+ // src/core/derived/diagnostics.ts
1761
+ var ANCESTOR_WALK_BOUND = 64;
1762
+ function describe(el) {
1763
+ return el.id ? `${el.tagName.toLowerCase()}#${el.id}` : el.tagName.toLowerCase();
1764
+ }
1765
+ function anyEstablishes3d(registry, names) {
1766
+ return names.some((name) => registry.resolve(name)?.preset.establishes3d === true);
1767
+ }
1768
+ function liveEstablishes3d(port, el) {
1769
+ const state = port.stateOf(el);
1770
+ if (!state || state.aggregate) return false;
1771
+ return anyEstablishes3d(port.registry, state.fxNames);
1772
+ }
1773
+ function find3dNeighbor(port, host, match) {
1774
+ if (liveEstablishes3d(port, host)) return host;
1775
+ let ancestor = match.parentElement;
1776
+ let depth = 0;
1777
+ while (ancestor && depth < ANCESTOR_WALK_BOUND) {
1778
+ if (ancestor !== host && liveEstablishes3d(port, ancestor)) return ancestor;
1779
+ ancestor = ancestor.parentElement;
1780
+ depth++;
1781
+ }
1782
+ return void 0;
1783
+ }
1784
+ function markUnmatched(host, selectors, attributes) {
1785
+ if (selectors.length === 0) {
1786
+ host.removeAttribute(ATTR.unmatched);
1787
+ return;
1788
+ }
1789
+ const value = selectors.join(", ");
1790
+ if (attributes) attributes.set(ATTR.unmatched, value);
1791
+ else host.setAttribute(ATTR.unmatched, value);
1792
+ }
1793
+ function warnNested3d(port, host, match, target) {
1794
+ if (!anyEstablishes3d(port.registry, target.plan.fxNames)) return;
1795
+ const neighbor = find3dNeighbor(port, host, match);
1796
+ if (!neighbor) return;
1797
+ const where = neighbor === host ? "its target: host" : "an ancestor";
1798
+ port.reporter.warn(
1799
+ `<${describe(match)}> establishes a 3D rendering context nested inside ${where} <${describe(neighbor)}>, which also runs a 3D-establishing effect \u2014 preserve-3d/perspective contexts do not compose when nested, so move one of the two 3D effects off its descendant`,
1800
+ match
1801
+ );
1802
+ }
1803
+ function warnPageScopeCloak(port, host, target) {
1804
+ if (target.scope !== "page") return;
1805
+ if (!target.plan.fxNames.some((name) => port.registry.resolve(name)?.preset.cloak === true)) return;
1806
+ if (!host.ownerDocument.documentElement.hasAttribute(ATTR.cloak)) return;
1807
+ port.reporter.warn(
1808
+ `target "${target.selector}" (scope:page) on <${describe(host)}> carries a cloaked entrance \u2014 the pre-JS cloak selector only hides descendants of the element it is authored on, so a scope:page match outside <${describe(host)}>'s own subtree will flash unstyled before this library runs; use scope:self, or drop the cloaked preset from this group`,
1809
+ host
1810
+ );
1811
+ }
1812
+
1813
+ // src/core/derived/install.ts
1814
+ var FORMER_HOST = /* @__PURE__ */ new WeakMap();
1815
+ function rememberFormerHost(book, match, host) {
1816
+ let table = FORMER_HOST.get(book);
1817
+ if (!table) {
1818
+ table = /* @__PURE__ */ new WeakMap();
1819
+ FORMER_HOST.set(book, table);
1820
+ }
1821
+ table.set(match, host);
1822
+ }
1823
+ function formerHostOf(book, match) {
1824
+ return FORMER_HOST.get(book)?.get(match);
1825
+ }
1826
+ function targetKey(target) {
1827
+ return `${target.scope} ${target.selector}`;
1828
+ }
1829
+ function matchKey(targets) {
1830
+ return targets.length === 1 ? targetKey(targets[0]) : targets.map(targetKey).sort((a, b) => a.localeCompare(b)).join("");
1831
+ }
1832
+ function groupConfigFor(port, context, target, cache) {
1833
+ const cached2 = cache.get(target);
1834
+ if (cached2) return cached2;
1835
+ const config = { ...resolveGroupConfig(context.baseConfig, target.hoists) };
1836
+ config.activation = port.resolveActivation(context.host, config, target.plan);
1837
+ cache.set(target, config);
1838
+ return config;
1839
+ }
1840
+ function resolveGroups(port, context, targets, cache) {
1841
+ const live = [];
1842
+ const resolved = [];
1843
+ const unmatched = [];
1844
+ for (const target of targets) {
1845
+ if (target.selector === "") continue;
1846
+ const matches = port.resolveGroupMatches(context.host, target);
1847
+ if (matches.length === 0) unmatched.push(target.selector);
1848
+ resolved.push({ target, matches });
1849
+ live.push({
1850
+ target,
1851
+ config: groupConfigFor(port, context, target, cache),
1852
+ gateOwner: groupGateOwner(port, context, target),
1853
+ members: []
1854
+ });
1855
+ }
1856
+ return { live, resolved, unmatched };
1857
+ }
1858
+ function buildContext(request) {
1859
+ const baseConfig = request.baseConfig;
1860
+ return {
1861
+ host: request.el,
1862
+ fingerprint: request.fingerprint,
1863
+ parsed: request.parsed,
1864
+ baseConfig,
1865
+ timeline: baseConfig.timeline
1866
+ };
1867
+ }
1868
+ function installHostGroup(port, request, own) {
1869
+ if (own) {
1870
+ port.install({ ...request, document: { targets: [own], warnings: [] } });
1871
+ } else {
1872
+ port.installAggregate(request);
1873
+ }
1874
+ }
1875
+ function registerCleanup2(port, host, cleanup) {
1876
+ const cleanups = port.book.cleanups.get(host) ?? [];
1877
+ cleanups.push(cleanup);
1878
+ port.book.cleanups.set(host, cleanups);
1879
+ }
1880
+ function installedTargetFor(port, context, targets) {
1881
+ return targets.length === 1 ? targets[0] : compileUnion(port, context, targets);
1882
+ }
1883
+ function finalizeInstall(port, context, input) {
1884
+ const host = context.host;
1885
+ const hostState = port.stateOf(host);
1886
+ markUnmatched(host, [...input.unmatched, ...input.suspects ?? []], hostState.attributes);
1887
+ registerCleanup2(port, host, claimTargetGroupHost(host));
1888
+ restageTargets(port, host);
1889
+ bindGroups(port, host);
1890
+ for (const [match, targets] of input.claims) {
1891
+ warnNested3d(port, host, match, installedTargetFor(port, context, targets));
1892
+ }
1893
+ for (const group of input.resolved) warnPageScopeCloak(port, host, group.target);
1894
+ syncAggregate(port, host);
1895
+ }
1896
+ function installWithTargets(port, request) {
1897
+ const context = buildContext(request);
1898
+ const cache = /* @__PURE__ */ new Map();
1899
+ const { live, resolved, unmatched } = resolveGroups(port, context, request.document.targets, cache);
1900
+ port.book.contexts.set(context.host, context);
1901
+ port.book.groups.set(context.host, live);
1902
+ const claims = claimMatches(port, context.host, resolved);
1903
+ const own = request.document.targets.find((target) => target.selector === "");
1904
+ if (!own && claims.size === 0) {
1905
+ context.host.setAttribute(ATTR.state, "failed");
1906
+ markUnmatched(context.host, [...unmatched, ...request.document.suspectSelectors ?? []]);
1907
+ return;
1908
+ }
1909
+ installDerivedMatches(port, context, claims, cache);
1910
+ installHostGroup(port, request, own);
1911
+ finalizeInstall(port, context, { claims, resolved, unmatched, suspects: request.document.suspectSelectors });
1912
+ }
1913
+ function releaseMatch(port, host, match) {
1914
+ const { book } = port;
1915
+ rememberFormerHost(book, match, host);
1916
+ book.hosts.delete(match);
1917
+ book.derived.get(host)?.delete(match);
1918
+ for (const group of book.groups.get(host) ?? []) {
1919
+ const index = group.members.indexOf(match);
1920
+ if (index !== -1) group.members.splice(index, 1);
1921
+ }
1922
+ }
1923
+ function releaseHost(port, host) {
1924
+ const { book } = port;
1925
+ for (const match of [...book.derived.get(host) ?? []]) port.release(match);
1926
+ for (const cleanup of book.cleanups.get(host) ?? []) cleanup();
1927
+ book.derived.delete(host);
1928
+ book.groups.delete(host);
1929
+ book.contexts.delete(host);
1930
+ book.unions.delete(host);
1931
+ book.cleanups.delete(host);
1932
+ }
1933
+ function releaseDerived(port, el) {
1934
+ const host = port.book.hosts.get(el);
1935
+ if (host) {
1936
+ releaseMatch(port, host, el);
1937
+ return;
1938
+ }
1939
+ if (port.book.derived.has(el)) releaseHost(port, el);
1940
+ }
1941
+ function restageAfterTreeRelease(port, removed) {
1942
+ const formerHosts = /* @__PURE__ */ new Set();
1943
+ for (const el of removed) {
1944
+ const host = formerHostOf(port.book, el);
1945
+ if (host) formerHosts.add(host);
1946
+ }
1947
+ for (const host of formerHosts) {
1948
+ if (!port.stateOf(host)) continue;
1949
+ restageTargets(port, host);
1950
+ syncAggregate(port, host);
1951
+ }
1952
+ }
1953
+ var SKIP_LABEL = {
1954
+ host: "is the target: host itself",
1955
+ authored: "already carries its own data-kui and animates itself",
1956
+ claimed: "is already claimed by another target: host"
1957
+ };
1958
+ function rejectionReason(port, host, match) {
1959
+ if (match === host) return "host";
1960
+ if (match.hasAttribute(ATTR.source)) return "authored";
1961
+ const claimedBy = port.book.hosts.get(match);
1962
+ if (claimedBy !== void 0 && claimedBy !== host) return "claimed";
1963
+ return void 0;
1964
+ }
1965
+ function claimMatch(claims, target, match) {
1966
+ const existing = claims.get(match);
1967
+ if (existing) existing.push(target);
1968
+ else claims.set(match, [target]);
1969
+ }
1970
+ function reportSkips(port, host, target, skipped) {
1971
+ const names = target.plan.fxNames.join(", ");
1972
+ for (const [reason, count] of skipped) {
1973
+ const plural = count === 1 ? "match" : "matches";
1974
+ port.reporter.warn(
1975
+ `target "${target.selector}" (${names}) skipped ${count} ${plural} that ${SKIP_LABEL[reason]}`,
1976
+ host
1977
+ );
1978
+ }
1979
+ }
1980
+ function claimMatches(port, host, groups) {
1981
+ const claims = /* @__PURE__ */ new Map();
1982
+ for (const group of groups) {
1983
+ const skipped = /* @__PURE__ */ new Map();
1984
+ for (const match of group.matches) {
1985
+ const reason = rejectionReason(port, host, match);
1986
+ if (reason) skipped.set(reason, (skipped.get(reason) ?? 0) + 1);
1987
+ else claimMatch(claims, group.target, match);
1988
+ }
1989
+ reportSkips(port, host, group.target, skipped);
1990
+ }
1991
+ return claims;
1992
+ }
1993
+ function resolveClaim(port, context, entry, cache) {
1994
+ const { match, targets } = entry;
1995
+ const target = installedTargetFor(port, context, targets);
1996
+ return { match, targets, target, config: groupConfigFor(port, context, target, cache) };
1997
+ }
1998
+ function matchGateOwner(port, context, targets) {
1999
+ const grouped = targets.some((target) => groupGateOwner(port, context, target) !== void 0);
2000
+ return grouped ? context.host : void 0;
2001
+ }
2002
+ function recordClaim(port, context, claim2) {
2003
+ const { book } = port;
2004
+ book.hosts.set(claim2.match, context.host);
2005
+ const derived = book.derived.get(context.host) ?? /* @__PURE__ */ new Set();
2006
+ derived.add(claim2.match);
2007
+ book.derived.set(context.host, derived);
2008
+ const live = book.groups.get(context.host) ?? [];
2009
+ for (const target of claim2.targets) {
2010
+ live.find((group) => group.target === target)?.members.push(claim2.match);
2011
+ }
2012
+ }
2013
+ function installClaim(port, context, claim2) {
2014
+ port.install({
2015
+ el: claim2.match,
2016
+ fingerprint: `${context.fingerprint}\0${matchKey(claim2.targets)}`,
2017
+ parsed: { specs: claim2.target.specs, warnings: [] },
2018
+ config: claim2.config,
2019
+ document: { targets: [{ ...claim2.target, selector: "", scope: "self" }], warnings: [] },
2020
+ host: context.host,
2021
+ gateOwner: matchGateOwner(port, context, claim2.targets)
2022
+ });
2023
+ recordClaim(port, context, claim2);
2024
+ }
2025
+ function installDerivedMatches(port, context, claims, cache = /* @__PURE__ */ new Map()) {
2026
+ const installed = [];
2027
+ for (const [match, targets] of claims) {
2028
+ const claim2 = resolveClaim(port, context, { match, targets }, cache);
2029
+ installClaim(port, context, claim2);
2030
+ installed.push(claim2.match);
2031
+ }
2032
+ return installed;
2033
+ }
2034
+ function restageTargets(port, host) {
2035
+ const context = port.book.contexts.get(host);
2036
+ const live = port.book.groups.get(host);
2037
+ const hostState = port.stateOf(host);
2038
+ if (!context || !live || !hostState) return;
2039
+ const elementWide = elementStaggerKeys(context.parsed);
2040
+ const styleOf = (el) => el === host ? hostState.ledgers.style(host) : port.stateOf(el).ledgers.style(el);
2041
+ for (const group of live) {
2042
+ if (group.members.length === 0) continue;
2043
+ indexTargetGroup({
2044
+ host,
2045
+ matches: group.members,
2046
+ styleOf,
2047
+ keys: staggerKeysFor(group.target.hoists, elementWide),
2048
+ reporter: port.reporter
2049
+ });
2050
+ }
2051
+ }
2052
+
2053
+ // src/core/derived/late-matches.ts
2054
+ var ANCESTOR_WALK_BOUND2 = 64;
2055
+ function newMatchesFor(node, host, target) {
2056
+ const candidates = [...node.querySelectorAll(target.selector)];
2057
+ if (node.matches(target.selector)) candidates.unshift(node);
2058
+ return candidates.filter((el) => host.contains(el) && el !== host && !el.hasAttribute(ATTR.part));
2059
+ }
2060
+ function resolveNewGroups(node, host, groups) {
2061
+ const resolved = [];
2062
+ const before = /* @__PURE__ */ new Map();
2063
+ for (const group of groups) {
2064
+ before.set(group, group.members.length);
2065
+ if (group.target.scope !== "self") continue;
2066
+ const matches = newMatchesFor(node, host, group.target);
2067
+ if (matches.length > 0) resolved.push({ target: group.target, matches });
2068
+ }
2069
+ return { resolved, before };
2070
+ }
2071
+ function insertionIndex(members, anchor) {
2072
+ const index = members.findIndex((existing) => Boolean(existing.compareDocumentPosition(anchor) & Node.DOCUMENT_POSITION_PRECEDING));
2073
+ return index === -1 ? members.length : index;
2074
+ }
2075
+ function hasAlreadyFired(port, group, added) {
2076
+ return group.members.some((member) => !added.includes(member) && port.stateOf(member)?.status !== "ready");
2077
+ }
2078
+ function activateIfAlreadyFired(port, group, added) {
2079
+ if (group.gateOwner === void 0) return;
2080
+ if (hasAlreadyFired(port, group, added)) for (const member of added) port.activateOne(member);
2081
+ }
2082
+ function reindexGroup(port, group, before) {
2083
+ if (group.members.length === before) return;
2084
+ const added = group.members.splice(before);
2085
+ group.members.splice(insertionIndex(group.members, added[0]), 0, ...added);
2086
+ activateIfAlreadyFired(port, group, added);
2087
+ }
2088
+ function adoptIntoHost(port, node, host) {
2089
+ const groups = port.book.groups.get(host);
2090
+ const context = port.book.contexts.get(host);
2091
+ if (!groups || !context) return;
2092
+ const { resolved, before } = resolveNewGroups(node, host, groups);
2093
+ if (resolved.length === 0) return;
2094
+ const claims = claimMatches(port, host, resolved);
2095
+ if (claims.size === 0) return;
2096
+ installDerivedMatches(port, context, claims);
2097
+ for (const group of groups) reindexGroup(port, group, before.get(group));
2098
+ restageTargets(port, host);
2099
+ }
2100
+ function adoptLateMatches(port, node) {
2101
+ if (node.hasAttribute(ATTR.part)) return;
2102
+ let ancestor = node.parentElement;
2103
+ let depth = 0;
2104
+ while (ancestor && depth < ANCESTOR_WALK_BOUND2) {
2105
+ adoptIntoHost(port, node, ancestor);
2106
+ ancestor = ancestor.parentElement;
2107
+ depth++;
2108
+ }
2109
+ }
2110
+
2111
+ // src/core/dom-watcher.ts
2112
+ var WORK_BUDGET = 100;
2113
+ function createDomWatcher(options) {
2114
+ const { root, onElementAdded, onElementRemoved, onAttributeChanged } = options;
2115
+ const schedule = options.schedule ?? scheduleFrame;
2116
+ const createObserver = options.createObserver ?? defaultObserverFactory();
2117
+ const added = /* @__PURE__ */ new Set();
2118
+ const removed = /* @__PURE__ */ new Set();
2119
+ const changed = /* @__PURE__ */ new Set();
2120
+ let observer;
2121
+ let scheduled = false;
2122
+ let destroyed = false;
2123
+ function collect(record) {
2124
+ if (record.type === "attributes") {
2125
+ const target = asElement(record.target);
2126
+ if (target) changed.add(target);
2127
+ return;
2128
+ }
2129
+ for (const node of record.addedNodes) queueRoot(added, asElement(node));
2130
+ for (const node of record.removedNodes) queueRoot(removed, asElement(node));
2131
+ }
2132
+ function queueWork(records) {
2133
+ for (const record of records) collect(record);
2134
+ if (scheduled) return;
2135
+ scheduled = true;
2136
+ schedule(flush);
2137
+ }
2138
+ const install = whileAttached(root, onElementAdded);
2139
+ const recompile = whileAttached(root, onAttributeChanged);
2140
+ function flush() {
2141
+ scheduled = false;
2142
+ if (destroyed) return;
2143
+ let remaining = WORK_BUDGET;
2144
+ remaining = drain(removed, onElementRemoved, remaining);
2145
+ remaining = drain(added, install, remaining);
2146
+ drain(changed, recompile, remaining);
2147
+ if (added.size + removed.size + changed.size > 0) {
2148
+ scheduled = true;
2149
+ schedule(flush);
2150
+ }
2151
+ }
2152
+ return {
2153
+ watch() {
2154
+ if (!createObserver) return;
2155
+ destroyed = false;
2156
+ observer = createObserver(queueWork);
2157
+ observer.observe(root, {
2158
+ subtree: true,
2159
+ childList: true,
2160
+ attributes: true,
2161
+ // `ATTR.stagger` is watched for the same reason `ATTR.source` is: both spell a stagger
2162
+ // group, and an edit to either has to re-rank the group. Its absence here meant editing
2163
+ // `data-kui-stagger` produced no record at all — not a missed handler, no mutation.
2164
+ attributeFilter: [ATTR.source, ATTR.on, ATTR.timeline, ATTR.threshold, ATTR.stagger]
2165
+ });
2166
+ },
2167
+ destroy() {
2168
+ destroyed = true;
2169
+ observer?.disconnect();
2170
+ added.clear();
2171
+ removed.clear();
2172
+ changed.clear();
2173
+ }
2174
+ };
2175
+ }
2176
+ function whileAttached(root, work) {
2177
+ return (el) => {
2178
+ if (root.contains(el)) work(el);
2179
+ };
2180
+ }
2181
+ function queueRoot(roots, candidate) {
2182
+ if (!candidate) return;
2183
+ for (const root of roots) {
2184
+ if (root.contains(candidate)) return;
2185
+ if (candidate.contains(root)) roots.delete(root);
2186
+ }
2187
+ roots.add(candidate);
2188
+ }
2189
+ function drain(roots, callback, budget) {
2190
+ for (const root of roots) {
2191
+ if (budget === 0) break;
2192
+ roots.delete(root);
2193
+ callback(root);
2194
+ budget--;
2195
+ }
2196
+ return budget;
2197
+ }
2198
+ function asElement(node) {
2199
+ return node.nodeType === 1 ? node : null;
2200
+ }
2201
+ function scheduleFrame(callback) {
2202
+ const requestFrame = globalThis.requestAnimationFrame;
2203
+ if (typeof requestFrame === "function") requestFrame(() => callback());
2204
+ else queueMicrotask(callback);
2205
+ }
2206
+ function defaultObserverFactory() {
2207
+ if (typeof MutationObserver === "undefined") return void 0;
2208
+ return (callback) => new MutationObserver(callback);
2209
+ }
2210
+
2211
+ // src/core/js-effect-preparer.ts
2212
+ function sequencedTiming(entry, warn) {
2213
+ const timing = readEffectTiming(entry.spec, warn);
2214
+ if (entry.sequencedDelayMs !== void 0) timing.delayMs = entry.sequencedDelayMs;
2215
+ return timing;
2216
+ }
2217
+ function createJsEffectPreparer(options) {
2218
+ const { scheduler, rootResolver, capabilities, reporter, respectReducedMotion } = options;
2219
+ function contextFor(el, signal, ledger) {
2220
+ const doc = el.ownerDocument;
2221
+ return {
2222
+ doc,
2223
+ win: doc.defaultView ?? globalThis,
2224
+ scheduler,
2225
+ rootFor: rootResolver,
2226
+ capabilities,
2227
+ invalidate: () => scheduler.invalidate(),
2228
+ warn: (message) => reporter.warn(message, el),
2229
+ reducedMotion: respectReducedMotion && capabilities.reducedMotion,
2230
+ signal,
2231
+ style: ledger
2232
+ };
2233
+ }
2234
+ return {
2235
+ prepare({ el, plan, signal, ledger }) {
2236
+ const instances = [];
2237
+ if (plan.jsEffects.length === 0) return instances;
2238
+ const ctx = contextFor(el, signal, ledger);
2239
+ for (const entry of plan.jsEffects) {
2240
+ const { spec, resolved } = entry;
2241
+ const prepare = resolved.primitive.prepare;
2242
+ if (!prepare) continue;
2243
+ const warn = (message) => reporter.warn(message, el);
2244
+ const params = readEffectParams(
2245
+ { ...resolved.preset.params, ...authoredParams(entry) },
2246
+ resolved.primitive.parameters,
2247
+ warn,
2248
+ sequencedTiming(entry, warn)
2249
+ );
2250
+ try {
2251
+ instances.push(prepare(el, params, ctx));
2252
+ } catch (error) {
2253
+ reporter.warn(`"${spec.name}" failed to initialise: ${String(error)}`, el);
2254
+ }
2255
+ }
2256
+ return instances;
2257
+ }
2258
+ };
2259
+ }
2260
+
2261
+ // src/core/reporter.ts
2262
+ function consoleReporter() {
2263
+ return {
2264
+ warn(message, subject) {
2265
+ if (subject === void 0) console.warn(`[kuinetic] ${message}`);
2266
+ else console.warn(`[kuinetic] ${message}`, subject);
2267
+ }
2268
+ };
2269
+ }
2270
+ function silentReporter() {
2271
+ return { warn() {
2272
+ } };
2273
+ }
2274
+ function collectingReporter() {
2275
+ const messages = [];
2276
+ return {
2277
+ messages,
2278
+ warn(message) {
2279
+ messages.push(message);
2280
+ }
2281
+ };
2282
+ }
2283
+
2284
+ // src/core/animator.ts
2285
+ var CLOAK_WATCHDOG_MS = 3e3;
2286
+ function fingerprintOf(attributes) {
2287
+ return [attributes.source, attributes.on, attributes.timeline, attributes.threshold].join("\0");
2288
+ }
2289
+ function isElementNode(node) {
2290
+ return typeof Element !== "undefined" && node instanceof Element;
2291
+ }
2292
+ var Animator = class {
2293
+ registry;
2294
+ capabilities;
2295
+ /**
2296
+ * Public alongside `registry` and `capabilities`, and for the same reason: `control()` is a free
2297
+ * function in its own module (mirroring `play()`), and an author-facing diagnostic it emits has
2298
+ * to reach the same sink every other diagnostic on this animator does. Routing it through a
2299
+ * second, private channel would mean `consoleReporter()` silenced half the library's warnings.
2300
+ */
2301
+ reporter;
2302
+ root;
2303
+ /**
2304
+ * Author-defined bundles found in this animator's root. Per-animator, not per-module: two
2305
+ * animators on two roots must not see each other's definitions.
2306
+ */
2307
+ bundles;
2308
+ binder;
2309
+ scheduler;
2310
+ rootResolver;
2311
+ jsEffectPreparer;
2312
+ respectReducedMotion;
2313
+ shouldObserve;
2314
+ /** Runtime truth. Attributes are for CSS and debugging; they make a poor state machine. */
2315
+ states = /* @__PURE__ */ new WeakMap();
2316
+ /** Iterable lifecycle index; the WeakMap remains the fast state lookup. */
2317
+ liveElements = /* @__PURE__ */ new Set();
2318
+ /**
2319
+ * Which run is current for one element's `InstanceState`, so a completion that resolves after
2320
+ * being superseded can tell it is stale even when `status` and `direction` do not say so.
2321
+ *
2322
+ * `state.direction` cannot serve as that identifier on its own — it only ever holds `'forward'`
2323
+ * or `'reverse'`, so an element whose activation flips back and forth (a hover-driven card flip,
2324
+ * `pointerenter`/`pointerleave` firing repeatedly) revisits the same direction many times over
2325
+ * its life. A cancelled run's completion settling late would then match a much later run headed
2326
+ * the same way, by coincidence rather than by being the same run. Keyed on `InstanceState` rather
2327
+ * than `Element` so a `release()`-then-reinstall — a fresh state object — starts with no
2328
+ * history, the same reason `settleWhen` already re-checks `this.states.get(el) !== state`.
2329
+ */
2330
+ currentRun = /* @__PURE__ */ new WeakMap();
2331
+ /**
2332
+ * Whether the element's current run has a settle gate that will eventually write its final
2333
+ * status, or whether nothing is ever going to report on it.
2334
+ *
2335
+ * `settleWhen` declines to arm a gate when every instance it was handed is `continuous` — see
2336
+ * the reasoning there, which is right: a pin that is still pinned should read `running`. But
2337
+ * "nobody will ever write this status" was left as an unrecorded fact known only inside that
2338
+ * one early return, and `cancel()` had no way to ask. So a cancelled pin kept `status =
2339
+ * 'running'` for the rest of the element's life and `activate()`'s own `running` guard locked it
2340
+ * out of every future activation. Measured on `<div data-kui="pin-section">`, the documented
2341
+ * form: cancel left `data-kui-state="running"`, and the next `activate()` emitted no `kui:start`
2342
+ * at all.
2343
+ *
2344
+ * Recorded here rather than re-derived in `cancel()` from `state.instances`, because the two
2345
+ * would not be asking the same question. `settleWhen` decides on the instances that actually
2346
+ * *started* this run; an instance whose setup threw is excluded there, and by cancel time it is
2347
+ * indistinguishable from a timed one (`continuous` reads from the setup result, which a failed
2348
+ * or torn-down instance no longer has). The gate's own answer is the only one that matches the
2349
+ * gate's own behaviour.
2350
+ *
2351
+ * Keyed on `InstanceState` for the same reason `currentRun` is: a `release()`-then-reinstall
2352
+ * mints a fresh state object, which must start with no history.
2353
+ */
2354
+ settleArmed = /* @__PURE__ */ new WeakMap();
2355
+ /** Built lazily by `watch()` when not injected, so nothing observes until `start()` needs it. */
2356
+ domWatcher;
2357
+ /**
2358
+ * Built lazily by `watchGates()`, and only for an element carrying a viewport gate on a
2359
+ * JavaScript-rendered effect — see `applyViewportGates` for why that is the only case that needs
2360
+ * one. Stays `undefined` on a page whose gates are all on CSS-rendered effects, which binds no
2361
+ * `MediaQueryList` listener anywhere.
2362
+ */
2363
+ gateWatcher;
2364
+ started = false;
2365
+ /** This animator's derived-host bookkeeping — see `DerivedBook`. */
2366
+ book = createDerivedBook();
2367
+ /** The narrow window the `derived/*` modules get onto this animator — see `AnimatorPort`. */
2368
+ port;
2369
+ constructor(options = {}) {
2370
+ const resolved = resolveCollaborators(options);
2371
+ this.registry = resolved.registry;
2372
+ this.bundles = new BundleTable(resolved.registry);
2373
+ this.capabilities = resolved.capabilities;
2374
+ this.root = resolved.root;
2375
+ this.reporter = resolved.reporter;
2376
+ this.binder = resolved.binder;
2377
+ this.scheduler = resolved.scheduler;
2378
+ this.rootResolver = resolved.rootResolver;
2379
+ this.jsEffectPreparer = resolved.jsEffectPreparer;
2380
+ this.domWatcher = resolved.domWatcher;
2381
+ this.respectReducedMotion = resolved.respectReducedMotion;
2382
+ this.shouldObserve = resolved.shouldObserve;
2383
+ this.port = {
2384
+ registry: this.registry,
2385
+ reporter: this.reporter,
2386
+ binder: this.binder,
2387
+ capabilities: this.capabilities,
2388
+ respectReducedMotion: this.respectReducedMotion,
2389
+ book: this.book,
2390
+ stateOf: (el) => this.states.get(el),
2391
+ install: (request) => this.install(request),
2392
+ installAggregate: (request) => this.installAggregate(request),
2393
+ resolveGroupMatches: (host, target) => this.resolveGroupMatches(host, target),
2394
+ resolveActivation: (el, config, plan) => this.resolveActivation(el, config, plan),
2395
+ release: (el) => this.release(el),
2396
+ activateOne: (el) => this.activateOne(el),
2397
+ deactivateOne: (el) => this.deactivate(el),
2398
+ writeStatus: (el, state, next) => this.writeStatus(el, state, next),
2399
+ emit: (el, state, type, reason) => this.emit(el, state, type, reason)
2400
+ };
2401
+ }
2402
+ /**
2403
+ * Explicit entry point. Importing the library never touches the document, which keeps SSR,
2404
+ * hydration, and tests deterministic.
2405
+ *
2406
+ * @complexity O(n) time in the number of elements scanned.
2407
+ * @overallScore 100
2408
+ */
2409
+ start() {
2410
+ if (this.started) return this;
2411
+ this.started = true;
2412
+ const watchdog = globalThis.setTimeout(() => this.uncloak(), CLOAK_WATCHDOG_MS);
2413
+ try {
2414
+ this.scan(this.root);
2415
+ if (this.shouldObserve) this.watch();
2416
+ } finally {
2417
+ globalThis.clearTimeout(watchdog);
2418
+ this.uncloak();
2419
+ }
2420
+ return this;
2421
+ }
2422
+ /**
2423
+ * Process every unprocessed element in a subtree, including the root.
2424
+ *
2425
+ * `querySelectorAll` excludes the root, but an inserted subtree very often carries the
2426
+ * attribute on its top node — skipping it silently drops those animations.
2427
+ *
2428
+ * Two passes, and the order is the feature. Every `data-kui-define` in the subtree is registered
2429
+ * before any element in it is compiled, so a `<template>` at the bottom of the document is
2430
+ * already known to the card at the top that names it. Making forward references work by
2431
+ * construction rather than by handling is the point — see `core/bundles.ts`.
2432
+ *
2433
+ * @param root - Subtree to scan. Defaults to the animator's root.
2434
+ * @complexity O(n) time in the subtree size; O(1) extra space.
2435
+ * @overallScore 100
2436
+ */
2437
+ scan(root = this.root) {
2438
+ if (!root) return this;
2439
+ const defined = this.bundles.collect(root, this.reporter);
2440
+ const selector = `[${ATTR.source}]`;
2441
+ if (isElementNode(root) && root.matches(selector)) this.process(root);
2442
+ for (const el of root.querySelectorAll(selector)) this.process(el);
2443
+ applyStagger(root, this.reporter);
2444
+ if (defined > 0 && root !== this.root) this.retryPending();
2445
+ return this;
2446
+ }
2447
+ /**
2448
+ * Recompile every element that stalled on a name the library did not know.
2449
+ *
2450
+ * `pending` is stamped for any unresolved name, not only an undefined bundle, so this can retry
2451
+ * an element that will fail again. That is deliberate: the alternative is an index of which
2452
+ * element is waiting on which name, and the cost of being wrong here is one wasted compile on a
2453
+ * page that already has a warning about it.
2454
+ *
2455
+ * @complexity O(p) time in the number of pending elements.
2456
+ * @overallScore 100
2457
+ */
2458
+ retryPending() {
2459
+ for (const el of this.root.querySelectorAll(`[${ATTR.state}="pending"]`)) this.process(el);
2460
+ }
2461
+ /**
2462
+ * Compile and install one element's effects, recompiling if its attribute changed.
2463
+ *
2464
+ * @param el - Element carrying `data-kui`.
2465
+ * @complexity O(e) time in the number of composed effects; O(e) space for the plan.
2466
+ * @overallScore 100
2467
+ */
2468
+ process(el) {
2469
+ if (el.hasAttribute(ATTR.define)) return;
2470
+ const attributes = readAttributes(el);
2471
+ const fingerprint = fingerprintOf(attributes);
2472
+ const existing = this.states.get(el);
2473
+ if (existing?.fingerprint === fingerprint) return;
2474
+ if (existing) this.release(el);
2475
+ const parsed = scopeHoists(this.bundles.expand(parse(attributes.source)), this.registry);
2476
+ const baseConfig = resolveConfig(attributes, parsed);
2477
+ const config = { ...baseConfig };
2478
+ const document2 = compileTargets(parsed, this.registry, baseConfig.timeline);
2479
+ const facts = document2.targets[0].plan;
2480
+ this.applyViewportGates(el, document2);
2481
+ config.activation = this.resolveActivation(el, baseConfig, facts);
2482
+ this.reportCompiled(el, document2);
2483
+ if (document2.targets.every((target) => target.plan.fxNames.length === 0)) {
2484
+ el.setAttribute(ATTR.state, facts.unknown.length > 0 ? "pending" : "failed");
2485
+ return;
2486
+ }
2487
+ this.install({ el, fingerprint, parsed, config, baseConfig, document: document2 });
2488
+ }
2489
+ /**
2490
+ * Report everything a finished compile has to say about one element.
2491
+ *
2492
+ * Three sources, one place. The third is separate from the plan's own warnings because `compile`
2493
+ * is pure and environment-free by design — it is handed a registry and a timeline, never the
2494
+ * browser it is running in. "This browser cannot render that channel" is only answerable here,
2495
+ * where the detected capabilities live, and `targets[0]`'s channel union is the merged one, so it
2496
+ * runs once per element rather than once per `target:` group.
2497
+ *
2498
+ * @complexity O(w) time in the total warning count; O(1) space.
2499
+ * @overallScore 100
2500
+ */
2501
+ reportCompiled(el, document2) {
2502
+ for (const warning of document2.warnings) this.reporter.warn(warning, el);
2503
+ for (const target of document2.targets) {
2504
+ for (const warning of target.plan.warnings) this.reporter.warn(warning, el);
2505
+ }
2506
+ const channels = [...new Set(document2.targets.flatMap((target) => target.plan.channels))];
2507
+ for (const warning of unsupportedChannelWarnings(channels, this.capabilities)) {
2508
+ this.reporter.warn(warning, el);
2509
+ }
2510
+ }
2511
+ /**
2512
+ * Apply a compiled plan and bind its activation.
2513
+ *
2514
+ * @complexity O(e) time in composed effects; O(e) space for retained cleanups.
2515
+ * @overallScore 100
2516
+ */
2517
+ /**
2518
+ * Choose the activation, letting a primitive's preference fill in only when the author named
2519
+ * none, and reporting whatever looks wrong about one the author did name.
2520
+ *
2521
+ * Declared capability metadata was previously never checked anywhere, which made
2522
+ * `supportedActivations` documentation rather than a contract. The checks themselves live in
2523
+ * `activation.ts`'s diagnostics half — they grew a good deal when the list opened, and they
2524
+ * decide nothing, so keeping them here would have made this class the place where "which
2525
+ * activation" and "is that a real event" were the same paragraph.
2526
+ *
2527
+ * @complexity O(a) time in supported activations; O(1) space.
2528
+ * @overallScore 100
2529
+ */
2530
+ resolveActivation(el, config, plan) {
2531
+ if (!config.activationAuthored) return plan.defaultActivation ?? config.activation;
2532
+ warnAboutActivation({
2533
+ el,
2534
+ spec: resolveActivationSpec(config.activation),
2535
+ supported: plan.supportedActivations,
2536
+ reporter: this.reporter
2537
+ });
2538
+ return config.activation;
2539
+ }
2540
+ /**
2541
+ * Resolve the viewport gates (`above:` / `below:`) on this element's JavaScript-rendered effects.
2542
+ *
2543
+ * CSS-rendered segments are deliberately absent from this method. Their gate is compiled into the
2544
+ * `animation-name` declaration as a `var()` the browser re-resolves on every resize with no
2545
+ * script involved (see `core/breakpoints.ts`), which is the entire reason this feature does not
2546
+ * need the teardown machinery `gsap.matchMedia()` is built around. A JavaScript-rendered effect
2547
+ * has no `animation-name` for a stylesheet to neutralise, so it is the one case that has to be
2548
+ * decided here — and, having been decided once, has to be re-decided when the viewport crosses
2549
+ * the breakpoint it was decided at.
2550
+ *
2551
+ * @complexity O(e) time in JS-rendered effects; O(b) space, bounded by the scale's five names.
2552
+ * @overallScore 100
2553
+ */
2554
+ applyViewportGates(el, document2) {
2555
+ const gates = document2.targets.flatMap((target) => target.plan.jsEffects).map((entry) => entry.spec.gate).filter((gate) => gate !== void 0);
2556
+ if (gates.length === 0) {
2557
+ this.gateWatcher?.unwatch(el);
2558
+ return;
2559
+ }
2560
+ const win = el.ownerDocument.defaultView ?? void 0;
2561
+ for (const target of document2.targets) {
2562
+ target.plan.jsEffects = target.plan.jsEffects.filter((entry) => gateMatches(entry.spec.gate, win));
2563
+ }
2564
+ this.watchGates(el, win, breakpointsIn(gates));
2565
+ }
2566
+ /**
2567
+ * Arm live re-evaluation for one element, building the watcher on first use.
2568
+ *
2569
+ * Lazy so that a page with no JavaScript-rendered gate — which is most pages, since the catalog
2570
+ * is overwhelmingly `css-keyframes` — binds no `MediaQueryList` listener at all.
2571
+ *
2572
+ * @complexity O(b) time in the element's breakpoints; O(1) amortised space.
2573
+ * @overallScore 100
2574
+ */
2575
+ watchGates(el, win, breakpoints) {
2576
+ this.gateWatcher ??= createGateWatcher(win, (target) => this.regate(target));
2577
+ this.gateWatcher.watch(el, breakpoints);
2578
+ }
2579
+ /**
2580
+ * Rebuild one element because the viewport crossed a breakpoint its JS effects depend on.
2581
+ *
2582
+ * `release` then `process`, rather than a partial update, and rather than `process` alone:
2583
+ * `process` short-circuits on an unchanged fingerprint, and the fingerprint is a hash of
2584
+ * *attributes*, which have not changed — the viewport has. Releasing first is also what makes the
2585
+ * transition safe mid-animation: it aborts the activation binding, runs each JS effect's own
2586
+ * `destroy()`, and unwinds both ledgers, so the element is back at the author's own markup before
2587
+ * the new plan touches it. That is the same path an attribute edit takes, so there is one
2588
+ * teardown implementation rather than two.
2589
+ *
2590
+ * There is deliberately no "is this element still live?" guard. Every path that stops tracking an
2591
+ * element goes through `release`, and `release` unwatches — so an element that reaches here is
2592
+ * one the watcher still holds, which is one `release` has not run on, which is one that is live.
2593
+ * A guard would be unreachable code pretending to be caution, the same call `positioned()` in
2594
+ * `compile.ts` makes about its own missing branch.
2595
+ *
2596
+ * @complexity O(e) time in the element's composed effects; O(1) space.
2597
+ * @overallScore 100
2598
+ */
2599
+ regate(el) {
2600
+ this.release(el);
2601
+ this.process(el);
2602
+ }
2603
+ /** Route a document with any non-empty-selector group through the derived-host path; `true`
2604
+ * when it did (a derived install's own `document` never carries one, so this cannot recurse). */
2605
+ installedAsHost(request) {
2606
+ if (!request.document.targets.some((t) => t.selector !== "")) return false;
2607
+ installWithTargets(this.port, request);
2608
+ return true;
2609
+ }
2610
+ install(request) {
2611
+ if (this.installedAsHost(request)) return;
2612
+ const { el, fingerprint, parsed, config, document: document2 } = request;
2613
+ const groups = document2.targets.map((target) => ({ target, matches: this.resolveGroupMatches(el, target) })).filter((group) => group.matches.length > 0);
2614
+ const ledgers = createLedgerSet(el);
2615
+ const ledger = ledgers.style(el);
2616
+ const attributes = ledgers.attributes(el);
2617
+ const controller = new AbortController();
2618
+ this.bindAuthorCallback(el, parsed, controller.signal);
2619
+ const elementHasCssAnimation = document2.targets.some(
2620
+ (target) => Object.keys(target.plan.declarations).length > 0
2621
+ );
2622
+ const elementStylePlan = planStyles({
2623
+ plan: groups[0].target.plan,
2624
+ config,
2625
+ capabilities: this.capabilities,
2626
+ respectReducedMotion: this.respectReducedMotion,
2627
+ elementHasCssAnimation
2628
+ });
2629
+ const state = {
2630
+ fingerprint,
2631
+ specs: parsed.specs,
2632
+ activation: config.activation,
2633
+ timeline: config.timeline,
2634
+ fxNames: document2.targets.flatMap((target) => target.plan.fxNames),
2635
+ jsEffectNames: document2.targets.flatMap(
2636
+ (target) => target.plan.jsEffects.map((entry) => entry.spec.name)
2637
+ ),
2638
+ progressDriven: elementStylePlan.gate === "scrubbed" || elementStylePlan.gate === "native-timeline",
2639
+ instances: [],
2640
+ ledger,
2641
+ attributes,
2642
+ ledgers,
2643
+ controller,
2644
+ status: "ready"
2645
+ };
2646
+ if (request.host) state.host = request.host;
2647
+ if (request.gateOwner) state.gateOwner = request.gateOwner;
2648
+ this.states.set(el, state);
2649
+ this.liveElements.add(el);
2650
+ attributes.set(ATTR.state, "ready");
2651
+ const context = {
2652
+ el,
2653
+ state,
2654
+ ledgers,
2655
+ config,
2656
+ signal: controller.signal,
2657
+ elementHasCssAnimation
2658
+ };
2659
+ for (const group of groups) this.installGroup(group, context);
2660
+ this.openGate({ el, state, stylePlan: elementStylePlan, config, plan: groups[0].target.plan });
2661
+ }
2662
+ /**
2663
+ * Install a zero-instance aggregate host: a `target:` host with no own (empty-selector) group,
2664
+ * whose status mirrors its derived matches' instead of running any effect of its own
2665
+ * (`syncAggregate`). Called by 2a's `installWithTargets` after every claimed match has already
2666
+ * been installed, which is what D-E's stamping order requires — the host's own `data-kui-state`
2667
+ * must never appear before its matches'.
2668
+ *
2669
+ * @complexity O(g) time in the document's group count; O(1) space.
2670
+ * @overallScore 100
2671
+ */
2672
+ installAggregate(request) {
2673
+ const { el, fingerprint, parsed, config, document: document2 } = request;
2674
+ const ledgers = createLedgerSet(el);
2675
+ const controller = new AbortController();
2676
+ this.bindAuthorCallback(el, parsed, controller.signal);
2677
+ const state = {
2678
+ fingerprint,
2679
+ specs: parsed.specs,
2680
+ activation: config.activation,
2681
+ timeline: config.timeline,
2682
+ fxNames: document2.targets.flatMap((target) => target.plan.fxNames),
2683
+ jsEffectNames: [],
2684
+ progressDriven: false,
2685
+ instances: [],
2686
+ ledger: ledgers.style(el),
2687
+ attributes: ledgers.attributes(el),
2688
+ ledgers,
2689
+ controller,
2690
+ status: "ready",
2691
+ aggregate: true
2692
+ };
2693
+ this.states.set(el, state);
2694
+ this.liveElements.add(el);
2695
+ state.attributes.set(ATTR.state, "ready");
2696
+ }
2697
+ /**
2698
+ * Apply one `target:` group's compiled plan to every element its selector resolved to.
2699
+ *
2700
+ * Its own `planStyles` call, not the element-wide one `install` already made: `declarations` are
2701
+ * per group, so `fade-up target:h2, pin` writes different properties to the `h2` than to the host
2702
+ * even though both share one gate, one activation and one reduced-motion policy (which is exactly
2703
+ * what `elementHasCssAnimation` carries in from the caller).
2704
+ *
2705
+ * `group.target.selector` is always `''` here — `installedAsHost` diverts any document carrying a
2706
+ * real `target:` group to `installWithTargets` before `install` (this method's only caller) ever
2707
+ * runs, so `matches` is always the host's own single-element `[el]` (target:-everywhere reconcile
2708
+ * R-8 removed the in-place relocation path this method used to also handle, along with the
2709
+ * `indexTargetGroup` call it made only for a real group — `restageTargets`, in
2710
+ * `core/derived/install.ts`, is the one that indexes a real target group's stagger keys now).
2711
+ *
2712
+ * @complexity O(m * p) time in the group's matches and the plan's properties; O(p) space.
2713
+ * @overallScore 100
2714
+ */
2715
+ installGroup(group, context) {
2716
+ const { target, matches } = group;
2717
+ const stylePlan = planStyles({
2718
+ plan: target.plan,
2719
+ config: context.config,
2720
+ capabilities: this.capabilities,
2721
+ respectReducedMotion: this.respectReducedMotion,
2722
+ elementHasCssAnimation: context.elementHasCssAnimation
2723
+ });
2724
+ const hasCssAnimation = Object.keys(stylePlan.properties).some(
2725
+ (property) => property.startsWith("animation-")
2726
+ );
2727
+ const writes = { target, stylePlan, hasCssAnimation };
2728
+ for (const match of matches) this.installMatch(match, writes, context);
2729
+ }
2730
+ /**
2731
+ * Write one group's plan onto one of its matched elements, and register the instances that plan
2732
+ * produced there.
2733
+ *
2734
+ * Every write goes through `context.ledgers`, never `match.style`/`match.setAttribute` directly:
2735
+ * under `scope:page` a match need not be a descendant of the host at all, so the ledger set is
2736
+ * the only thing that knows to unwind it on `release()`.
2737
+ *
2738
+ * @complexity O(p) time in the plan's property count; O(1) space beyond the instances pushed.
2739
+ * @overallScore 100
2740
+ */
2741
+ installMatch(match, writes, context) {
2742
+ const { target, stylePlan, hasCssAnimation } = writes;
2743
+ const { state, ledgers } = context;
2744
+ const matchLedger = ledgers.style(match);
2745
+ const matchAttributes = ledgers.attributes(match);
2746
+ for (const [property, value] of Object.entries(stylePlan.properties)) {
2747
+ matchLedger.set(property, value);
2748
+ }
2749
+ matchAttributes.set(ATTR.normalized, stylePlan.attributes[ATTR.normalized]);
2750
+ matchAttributes.set(ATTR.rm, stylePlan.attributes[ATTR.rm]);
2751
+ matchLedger.claim("animation-play-state");
2752
+ if (hasCssAnimation) {
2753
+ const scrubbed = stylePlan.gate === "scrubbed";
2754
+ state.instances.push(
2755
+ createCssInstance(match, matchLedger, target.plan.keyframeNames, scrubbed)
2756
+ );
2757
+ }
2758
+ state.instances.push(
2759
+ ...this.jsEffectPreparer.prepare({
2760
+ el: match,
2761
+ plan: target.plan,
2762
+ signal: context.signal,
2763
+ ledger: matchLedger
2764
+ })
2765
+ );
2766
+ }
2767
+ /**
2768
+ * Wire an authored `func:` to this element's completion.
2769
+ *
2770
+ * Registered as an ordinary `kui:finish` listener rather than called from a dispatch site, so it
2771
+ * can never fire at a moment the event does not — the key is sugar for that listener, and is
2772
+ * implemented as literally that listener. It detaches with every other binding on
2773
+ * `controller.abort()`.
2774
+ *
2775
+ * Called from `install` *before* `openGate`, and the order is load-bearing: the one `kui:finish`
2776
+ * an element can receive during install is the synchronous `reduced-motion` one, and the visitor
2777
+ * who asked for reduced motion is the last one whose chained step should silently be dropped.
2778
+ *
2779
+ * See `callback.ts` for the lookup rules, and for why this key must never be built from
2780
+ * untrusted input.
2781
+ *
2782
+ * @complexity O(1) time and space.
2783
+ * @overallScore 100
2784
+ */
2785
+ bindAuthorCallback(el, parsed, signal) {
2786
+ if (parsed.func === void 0) return;
2787
+ bindCallback({ el, name: parsed.func, reporter: this.reporter, signal });
2788
+ }
2789
+ /**
2790
+ * Validate and resolve one `CompiledTarget`'s selector against the live document.
2791
+ *
2792
+ * `resolveTarget`'s document-wide/invalid rejection — the same rule the six built-in
2793
+ * `target:`-declaring primitives apply inside their own `prepare` — has to run again here for
2794
+ * every *other* primitive: a CSS-rendered retargeted effect never calls `prepare` at all, so
2795
+ * compile-to-install time is the only point at which this element has a real `Document` to
2796
+ * validate the selector against. `compileTargets` itself is pure and DOM-free by design and
2797
+ * cannot do this check.
2798
+ *
2799
+ * @param el - The host. The search root under `'self'`, and the only "match" for the host group
2800
+ * (`target.selector === ''`), which is never queried at all.
2801
+ * @returns The matches, in document order. Empty when the selector was invalid, too broad, or
2802
+ * simply matched nothing — every case already warned by name.
2803
+ * @complexity O(1) for the host group; O(n) in document size otherwise, the query itself being
2804
+ * the DOM's.
2805
+ * @overallScore 100
2806
+ */
2807
+ resolveGroupMatches(el, target) {
2808
+ if (target.selector === "") return [el];
2809
+ const doc = el.ownerDocument;
2810
+ const names = target.plan.fxNames.join(", ");
2811
+ const breadth = selectorBreadth(target.selector, doc);
2812
+ if (breadth !== "ok") {
2813
+ const reason = breadth === "invalid" ? "is not a valid selector" : "matches the whole document";
2814
+ this.reporter.warn(`target "${target.selector}" (${names}) ${reason} and will be ignored`, el);
2815
+ return [];
2816
+ }
2817
+ const matches = queryScoped(el, { doc }, target.selector, target.scope);
2818
+ if (matches.length === 0) {
2819
+ this.reporter.warn(`target "${target.selector}" (${names}) matched nothing`, el);
2820
+ }
2821
+ return matches;
2822
+ }
2823
+ /**
2824
+ * Decide whether, and when, the effects on this element are allowed to start.
2825
+ *
2826
+ * The single place any effect begins. Routing both renderers through it is what makes
2827
+ * `on:enter`, `on:click`, `manual`, and `reducedMotion: 'disable'` mean the same thing for a
2828
+ * pinned section as for a fade — previously JS effects started during `prepare` and honoured
2829
+ * none of them.
2830
+ *
2831
+ * @complexity O(n) time in the number of instances; O(1) space.
2832
+ * @overallScore 100
2833
+ */
2834
+ openGate(request) {
2835
+ const { el, state, stylePlan, config, plan } = request;
2836
+ if (this.reducedMotionDisables(plan)) {
2837
+ this.writeStatus(el, state, "finished");
2838
+ this.emit(el, state, KUI_EVENT.finish, "reduced-motion");
2839
+ this.syncHost(state);
2840
+ return;
2841
+ }
2842
+ const actions = config.actions;
2843
+ if (actions) {
2844
+ this.warnAboutCrossings(el, state, stylePlan.activation ?? config.activation, actions);
2845
+ }
2846
+ if (stylePlan.gate !== "deferred") {
2847
+ this.activateOne(el);
2848
+ return;
2849
+ }
2850
+ if (skipsOwnBinding(state)) return;
2851
+ const activation = stylePlan.activation;
2852
+ const releaseBinding = this.binder.bind(el, activation, {
2853
+ threshold: config.threshold,
2854
+ from: config.activationSource,
2855
+ activate: () => this.activateOne(el),
2856
+ deactivate: () => this.deactivate(el),
2857
+ // Only when the author asked for the four-way reading. Absent, the binder keeps its two-way
2858
+ // delivery and its one-shot release, which is what every piece of existing markup depends on.
2859
+ ...actions ? { cross: (crossing) => this.applyCrossing(el, crossing, actions) } : {}
2860
+ });
2861
+ let released = false;
2862
+ const releaseOnce = () => {
2863
+ if (released) return;
2864
+ released = true;
2865
+ releaseBinding();
2866
+ };
2867
+ state.controller.signal.addEventListener("abort", releaseOnce);
2868
+ if (!actions && isOneShot(resolveActivationSpec(activation))) {
2869
+ state.releaseActivation = releaseOnce;
2870
+ }
2871
+ }
2872
+ /**
2873
+ * Whether the visitor's own reduced-motion preference disables this plan outright, rather than
2874
+ * merely shortening or crossfading it.
2875
+ *
2876
+ * Pulled out of `openGate` so that method's own branching stays about *what to do*, not about
2877
+ * the two-part question of whether the policy applies at all.
2878
+ *
2879
+ * @complexity O(1) time and space.
2880
+ * @overallScore 100
2881
+ */
2882
+ reducedMotionDisables(plan) {
2883
+ return this.respectReducedMotion && this.capabilities.reducedMotion && plan.reducedMotion === "disable";
2884
+ }
2885
+ /**
2886
+ * Report every way an authored `actions:` cannot do what it says, once, at bind time.
2887
+ *
2888
+ * At bind time rather than per crossing, and that is the whole reason this is a separate method:
2889
+ * a crossing fires on every scroll pass, and a diagnostic repeated on each one is a diagnostic
2890
+ * nobody reads. It is the same discipline `control.ts` applies — warn once at construction, even
2891
+ * for a caller who only meant to read.
2892
+ *
2893
+ * @complexity O(1) time and space.
2894
+ * @overallScore 100
2895
+ */
2896
+ warnAboutCrossings(el, state, activation, actions) {
2897
+ const spec = resolveActivationSpec(activation);
2898
+ const problems = warnAboutToggleActions({
2899
+ actions,
2900
+ observed: spec.start.kind === "observed" || spec.end?.kind === "observed",
2901
+ activation: String(activation),
2902
+ jsEffectNames: state.jsEffectNames,
2903
+ progressDriven: state.progressDriven
2904
+ });
2905
+ for (const problem of problems) this.reporter.warn(problem, el);
2906
+ }
2907
+ /**
2908
+ * Do whatever this element's `actions:` names for the crossing that just happened.
2909
+ *
2910
+ * A pure dispatch, and deliberately nothing more. `toggle-actions.ts` owns what each verb means,
2911
+ * `activation.ts` owns which crossing it was, and this method's only contribution is the two
2912
+ * things neither of them can reach: the element's own directional transitions, and its instances'
2913
+ * playheads. If anything resembling animation logic ever appears here, it is in the wrong file.
2914
+ *
2915
+ * @complexity O(n) time in the element's instances; O(n) space.
2916
+ * @overallScore 100
2917
+ */
2918
+ applyCrossing(el, crossing, actions) {
2919
+ const state = this.states.get(el);
2920
+ if (!state) return;
2921
+ applyToggleVerb(actions[crossing], {
2922
+ // `ready` is the only status that has not started; `finished` and `failed` both have, and a
2923
+ // `resume` on either of those is a resume rather than a fresh activation.
2924
+ started: state.status !== "ready",
2925
+ controls: state.instances.map((instance) => instance.control).filter((control2) => control2 !== void 0),
2926
+ activate: () => this.activateOne(el),
2927
+ reverse: () => this.reverseOne(el)
2928
+ });
2929
+ }
2930
+ /**
2931
+ * Start a deferred animation.
2932
+ *
2933
+ * A JS-rendered effect's real setup work is postponed until this call — `deferPrepare` in
2934
+ * `instances.ts` only wires up an inert instance during `prepare` — so a broken primitive (a bad
2935
+ * selector, a malformed param) first throws here, not while the plan was being built. `scan()`
2936
+ * reaches this synchronously for every `on:load` element, inside the very loop that processes
2937
+ * every other element on the page; an uncaught throw here previously unwound that loop and
2938
+ * silently orphaned every element after the broken one — the same blast radius the `__proto__`
2939
+ * scan-crash fix closed for a different door. Each instance is isolated so one effect's failure
2940
+ * can neither strand a sibling effect on the same element nor abort the rest of the scan.
2941
+ *
2942
+ * @complexity O(n) time in composed instances; O(1) space.
2943
+ * @overallScore 100
2944
+ */
2945
+ activate(el) {
2946
+ this.activateOne(el);
2947
+ for (const match of this.derivedOf(el)) this.activateOne(match);
2948
+ }
2949
+ /** The per-element half of {@link activate} — see that method's own doc comment. */
2950
+ activateOne(el) {
2951
+ const state = this.states.get(el);
2952
+ if (!state) return;
2953
+ if (state.aggregate) return;
2954
+ if (state.status === "running" && state.direction === "reverse") {
2955
+ this.turnAround(el, state);
2956
+ return;
2957
+ }
2958
+ if (state.status === "running") return;
2959
+ state.releaseActivation?.();
2960
+ state.releaseActivation = void 0;
2961
+ this.writeStatus(el, state, "running");
2962
+ state.direction = "forward";
2963
+ const run = this.beginRun(state);
2964
+ const started = state.instances.filter((instance) => this.startInstance(instance, el));
2965
+ if (started.length === 0 && state.instances.length > 0) {
2966
+ this.writeStatus(el, state, "failed");
2967
+ this.syncHost(state);
2968
+ return;
2969
+ }
2970
+ this.emit(el, state, KUI_EVENT.start, "activated");
2971
+ this.syncHost(state);
2972
+ this.settleWhen({ el, state, run }, started, "finished");
2973
+ }
2974
+ /**
2975
+ * Play an element's effects back out.
2976
+ *
2977
+ * The exit half of a paired activation — `data-kui-on="pointerenter/pointerleave"`,
2978
+ * `data-kui-on="enter/leave"` — routed through one method for the same reason `activate` is: it
2979
+ * is the single place an effect ever runs backwards, so what an exit means does not have to be
2980
+ * re-decided per activation.
2981
+ *
2982
+ * **CSS-rendered effects reverse; JS-rendered ones do not, and say so.** A CSS effect has a real
2983
+ * `Animation` handle whose playback rate can simply be negated, landing on the from-state that
2984
+ * `animation-fill-mode: both` is already holding. A JS-rendered effect has no playhead at all —
2985
+ * `getAnimations()` returns `[]` for it (see `play.ts`) — and there is no honest general shim
2986
+ * for "half a `split-flap`, backwards". Rather than invent one that misbehaves differently per
2987
+ * primitive, an instance that cannot reverse is named in a warning; the author learns their
2988
+ * pointerleave does nothing *here* instead of discovering it in a browser. On an element
2989
+ * composing both renderers the CSS half still reverses and the warning names what did not.
2990
+ *
2991
+ * @param el - Element whose effects should play out.
2992
+ * @complexity O(n) time in the number of instances; O(1) space.
2993
+ * @overallScore 100
2994
+ */
2995
+ deactivate(el) {
2996
+ const state = this.states.get(el);
2997
+ if (!state) return;
2998
+ if (state.status !== "running" && state.status !== "finished") return;
2999
+ if (state.direction === "reverse") return;
3000
+ const reversible = state.instances.filter(isDirectional);
3001
+ if (reversible.length < state.instances.length) {
3002
+ this.reporter.warn(
3003
+ "effect cannot play backwards, so the exit half of this activation does nothing for it (JS-rendered effects have no playhead \u2014 see EffectInstance.reverse)",
3004
+ el
3005
+ );
3006
+ }
3007
+ this.reverseOne(el);
3008
+ }
3009
+ /**
3010
+ * Turn an element's playhead around and settle it back at the from-state it started from.
3011
+ *
3012
+ * The single owner of `state.direction === 'reverse'`, and it exists as its own method because
3013
+ * for a while there were two owners and neither knew about the other. `deactivate()` — the exit
3014
+ * half of a paired activation — flipped the direction and re-armed the settle gate;
3015
+ * `control(el).reverse()` reached straight past the state machine into the raw `Animation`
3016
+ * handles through `InstanceControl.reverse`. Both moved the same playheads, only one of them said
3017
+ * so, and an animator that believed a reversing element was still travelling forwards got three
3018
+ * things wrong at once: a later `deactivate()` sailed through its "an exit already in flight must
3019
+ * not restart" guard and began a *second* reverse; `activate()` could never turn the playhead
3020
+ * around, because it looks for `'reverse'` and never saw it; and the forward `settleWhen` left
3021
+ * over from the entrance stayed pending until it stamped `data-kui-state="finished"` onto an
3022
+ * element sitting at its from-state.
3023
+ *
3024
+ * So the transition lives here and both entry points call it. What `deactivate()` keeps is only
3025
+ * what is true of an *activation* exit specifically. A programmatic reverse is not one — an
3026
+ * author calling `control().reverse()` has not had a pointer leave anything — so it inherits
3027
+ * none of that, and in particular is not told that the effects it cannot reach make "the exit
3028
+ * half of this activation" do nothing. `control.ts` has already named those for it.
3029
+ *
3030
+ * Resuming forward travel is `activate()`'s job, not a second method here: an element left in
3031
+ * `'reverse'` is turned around by `turnAround`, which is reachable from every route that
3032
+ * activates — a pointer coming back, `play()`, `kui.activate()`.
3033
+ *
3034
+ * @param el - Element whose effects should run backwards. Unknown elements are ignored, exactly
3035
+ * as they are by `activate` and `deactivate`.
3036
+ * @complexity O(n) time in the number of instances; O(1) space.
3037
+ * @overallScore 100
3038
+ */
3039
+ reverseFrom(el) {
3040
+ this.reverseOne(el);
3041
+ for (const match of this.derivedOf(el)) this.reverseOne(match);
3042
+ }
3043
+ /** The per-element half of {@link reverseFrom} — see that method's own doc comment. */
3044
+ reverseOne(el) {
3045
+ const state = this.states.get(el);
3046
+ if (!state) return;
3047
+ if (state.status !== "running" && state.status !== "finished") return;
3048
+ if (state.direction === "reverse") return;
3049
+ const reversible = state.instances.filter(isDirectional);
3050
+ if (reversible.length === 0) return;
3051
+ this.writeStatus(el, state, "running");
3052
+ state.direction = "reverse";
3053
+ this.syncHost(state);
3054
+ const run = this.beginRun(state);
3055
+ for (const instance of reversible) instance.reverse();
3056
+ this.settleWhen({ el, state, run }, reversible, "ready");
3057
+ }
3058
+ /**
3059
+ * Resume forward playback on an element whose exit is still in flight.
3060
+ *
3061
+ * Separate from `activate`'s main path because none of that path applies: the instances are
3062
+ * already started, the activation is already spent, and the state is already `running`. All that
3063
+ * changes is the direction of travel.
3064
+ *
3065
+ * @complexity O(n) time in the number of instances; O(1) space.
3066
+ * @overallScore 100
3067
+ */
3068
+ turnAround(el, state) {
3069
+ const playable = state.instances.filter(isDirectional);
3070
+ state.direction = "forward";
3071
+ const run = this.beginRun(state);
3072
+ for (const instance of playable) instance.play();
3073
+ this.settleWhen({ el, state, run }, playable, "finished");
3074
+ }
3075
+ /**
3076
+ * Write an element's final state once the instances driving it have finished.
3077
+ *
3078
+ * The reported state has to become truthful eventually, or `data-kui-state` codifies a lie that
3079
+ * tests then assert against. It also has to stay truthful when a run is superseded mid-flight: a
3080
+ * reverse that is turned around leaves its own `finished` promise pending, and letting that
3081
+ * promise write `ready` over the forward run that replaced it is how an element ends up claiming
3082
+ * it is back at its from-state while visibly finishing its entrance. Capturing the direction at
3083
+ * the time of the call and re-checking it on resolution is what makes the stale promise a no-op
3084
+ * in the ordinary case — turned-around and reversed-again runs, which is why that comment and
3085
+ * check are kept even though `run` below also covers them.
3086
+ *
3087
+ * `run` is the check `direction` cannot be: `direction` only ever holds two values, so a run
3088
+ * that flips back and forth several times (a hover-driven card flip, `pointerenter`/
3089
+ * `pointerleave` firing repeatedly) revisits the same direction repeatedly, and a stale run's
3090
+ * belated completion could otherwise be mistaken for a much later one simply travelling the same
3091
+ * way. `beginRun` mints one identity per run; only the run that is still current when its own
3092
+ * promise resolves gets to report anything.
3093
+ *
3094
+ * @param target - The element, its state, and this run's identity — see {@link SettleTarget}.
3095
+ * @param instances - The instances this particular run started; a run only waits on its own.
3096
+ * @param next - State to report when they are all done.
3097
+ * @complexity O(n) time in the instances; O(n) space for the pending promises.
3098
+ * @overallScore 100
3099
+ */
3100
+ settleWhen(target, instances, next) {
3101
+ const { el, state, run } = target;
3102
+ const timed = instances.filter((instance) => !instance.continuous);
3103
+ if (timed.length === 0 && instances.length > 0) {
3104
+ this.settleArmed.set(state, false);
3105
+ return;
3106
+ }
3107
+ this.settleArmed.set(state, true);
3108
+ const direction = state.direction;
3109
+ void Promise.all(timed.map((instance) => instance.finished)).then(() => {
3110
+ if (this.states.get(el) !== state || state.status !== "running") return;
3111
+ if (state.direction !== direction) return;
3112
+ if (this.currentRun.get(state) !== run) return;
3113
+ this.writeStatus(el, state, next);
3114
+ if (!state.cancelled) {
3115
+ if (next === "finished") this.emit(el, state, KUI_EVENT.finish, "complete");
3116
+ else this.emit(el, state, KUI_EVENT.reverseFinish, "reversed");
3117
+ }
3118
+ this.syncHost(state);
3119
+ });
3120
+ }
3121
+ /**
3122
+ * Mark the start of a new run — a forward activation, a reversal, or a turnaround out of one —
3123
+ * and clear the mark a previous run's cancellation left behind.
3124
+ *
3125
+ * The two things a fresh run does are bound together deliberately: `cancelled` describes *this*
3126
+ * run, not the element for life, so every place that begins one must also disown whatever the
3127
+ * run before it was marked with, or that run's cancellation would silently apply to a completion
3128
+ * it had nothing to do with — see `InstanceState.cancelled`'s own doc comment for the contract
3129
+ * this keeps. Doing both here, in one call, is what stops a caller resetting one without the
3130
+ * other; three call sites each remembering to write two lines in the right order was exactly
3131
+ * this bug's shape the first time.
3132
+ *
3133
+ * @returns This run's identity, to be threaded through to the `settleWhen` call that will
3134
+ * eventually report on it.
3135
+ * @complexity O(1) time, O(1) space.
3136
+ * @overallScore 100
3137
+ */
3138
+ beginRun(state) {
3139
+ const run = Symbol("kui-run");
3140
+ this.currentRun.set(state, run);
3141
+ state.cancelled = false;
3142
+ return run;
3143
+ }
3144
+ /**
3145
+ * Dispatch one lifecycle event for an element, filling in the identity every listener needs.
3146
+ *
3147
+ * Centralised here rather than inside each instance because the animator is the only place that
3148
+ * knows the *element's* lifecycle — an element composing three effects starts once and finishes
3149
+ * once, not three times, and only this class sees all three instances at the same moment.
3150
+ *
3151
+ * @complexity O(n) time in listeners on the propagation path; O(1) space.
3152
+ * @overallScore 100
3153
+ */
3154
+ emit(el, state, type, reason) {
3155
+ emitLifecycle(el, type, {
3156
+ effects: state.fxNames,
3157
+ activation: state.activation,
3158
+ timeline: state.timeline,
3159
+ reason,
3160
+ ...state.host ? { host: state.host } : {}
3161
+ });
3162
+ }
3163
+ /**
3164
+ * Write one element's status, through its own ledger, with no aggregate sync.
3165
+ *
3166
+ * The one place `status`/`data-kui-state` are written together, so every call site that used to
3167
+ * write both fields by hand now shares one line to drift out of sync in. `el` is unused today —
3168
+ * kept because 2b's tests and a future per-element hook both key on it, and because every other
3169
+ * write in this file takes the element it writes to as its first argument.
3170
+ *
3171
+ * @complexity O(1) time and space.
3172
+ * @overallScore 100
3173
+ */
3174
+ writeStatus(el, state, next) {
3175
+ state.status = next;
3176
+ state.attributes.set(ATTR.state, next);
3177
+ }
3178
+ /**
3179
+ * Let a derived state's host re-derive its aggregate status (D-A).
3180
+ *
3181
+ * Called after the derived element's own event, so listeners see the match's event before the
3182
+ * host's — the same order a nested DOM structure would deliver them in were the host merely
3183
+ * listening for its children's events instead of deriving its own status from them.
3184
+ *
3185
+ * @complexity O(1) time and space, delegating to {@link syncAggregate}.
3186
+ * @overallScore 100
3187
+ */
3188
+ syncHost(state) {
3189
+ if (state.host) syncAggregate(this.port, state.host);
3190
+ }
3191
+ /**
3192
+ * Stop one element's effects where they are, leaving it mid-animation.
3193
+ *
3194
+ * The counterpart to `activate()`, and the reason it exists rather than callers reaching into
3195
+ * `stateOf(el).instances` themselves (which is what `play()`'s handle used to do): cancellation
3196
+ * has an observable consequence — `kui:cancel`, and the suppression of the `kui:finish` that
3197
+ * would otherwise follow it — and that consequence has to be applied wherever cancellation
3198
+ * happens, not only where it happens to be convenient.
3199
+ *
3200
+ * @param el - Element whose effects should stop.
3201
+ * @complexity O(n) time in the element's instances; O(1) space.
3202
+ * @overallScore 100
3203
+ */
3204
+ cancel(el) {
3205
+ this.cancelOne(el);
3206
+ for (const match of this.derivedOf(el)) this.cancelOne(match);
3207
+ }
3208
+ /** The per-element half of {@link cancel} — see that method's own doc comment. */
3209
+ cancelOne(el) {
3210
+ const state = this.states.get(el);
3211
+ if (!state) return;
3212
+ const wasRunning = state.status === "running";
3213
+ state.cancelled = true;
3214
+ for (const instance of state.instances) runQuietly(() => instance.cancel());
3215
+ if (wasRunning && this.settleArmed.get(state) === false) {
3216
+ this.writeStatus(el, state, "finished");
3217
+ this.syncHost(state);
3218
+ }
3219
+ if (wasRunning) this.emit(el, state, KUI_EVENT.cancel, "cancelled");
3220
+ }
3221
+ /**
3222
+ * Runtime control over a selection's playheads — pause, resume, reverse, seek, re-speed.
3223
+ *
3224
+ * Selection-shaped rather than single-element, so it matches `play()` and so one call covers a
3225
+ * staggered group. `control.ts` builds a per-element handle underneath and the returned handle
3226
+ * composes them.
3227
+ *
3228
+ * @param target - Selector, element, or iterable of elements.
3229
+ * @complexity O(n) time in selected elements and their instances; O(n) space.
3230
+ * @overallScore 100
3231
+ */
3232
+ control(target) {
3233
+ return control({ animator: this, root: this.root, target });
3234
+ }
3235
+ /**
3236
+ * Activate one instance, isolating a throw from its (possibly deferred) setup.
3237
+ *
3238
+ * @returns Whether the instance actually started — a failed instance is excluded from the
3239
+ * `finished` gate in `activate()`, since something that never started can never legitimately
3240
+ * finish (see `EffectInstance.finished`'s "resolves, never rejects" contract in `types.ts`).
3241
+ * @complexity O(1) time, O(1) space.
3242
+ * @overallScore 100
3243
+ */
3244
+ startInstance(instance, el) {
3245
+ try {
3246
+ instance.activate();
3247
+ return true;
3248
+ } catch (error) {
3249
+ this.reporter.warn(`effect failed to activate: ${String(error)}`, el);
3250
+ return false;
3251
+ }
3252
+ }
3253
+ /**
3254
+ * Programmatic entry point. Accepts a selector, an Element, a NodeList, or any iterable, so
3255
+ * `getElementById`, `getElementsByClassName`, and `querySelectorAll` all work directly.
3256
+ *
3257
+ * @complexity O(n) time in selected elements.
3258
+ * @overallScore 100
3259
+ */
3260
+ play(target, effect, options = {}) {
3261
+ return play({ animator: this, root: this.root, target, effect }, options);
3262
+ }
3263
+ /**
3264
+ * Remove the opt-in cloak so a stalled or failed initialisation can never leave a page hidden.
3265
+ *
3266
+ * @complexity O(1) time, O(1) space.
3267
+ * @overallScore 100
3268
+ */
3269
+ uncloak() {
3270
+ const doc = isElementNode(this.root) ? this.root.ownerDocument : this.root;
3271
+ doc?.documentElement?.removeAttribute(ATTR.cloak);
3272
+ }
3273
+ stateOf(el) {
3274
+ return this.states.get(el);
3275
+ }
3276
+ /** The derived matches a host's `target:` groups installed, in document order. `[]` otherwise. */
3277
+ derivedOf(el) {
3278
+ return derivedOf(this.book, el);
3279
+ }
3280
+ /** The host that installed a derived match, else `undefined`. */
3281
+ hostOf(el) {
3282
+ return hostOf(this.book, el);
3283
+ }
3284
+ /**
3285
+ * Tear an element's effects down so the next `process()` reinstalls from scratch.
3286
+ *
3287
+ * Needed for replay: `process()` short-circuits when the configuration fingerprint is
3288
+ * unchanged, so playing the same effect twice was previously a no-op.
3289
+ *
3290
+ * @complexity O(c) time in retained instances; O(1) space.
3291
+ * @overallScore 100
3292
+ */
3293
+ reset(el) {
3294
+ this.release(el);
3295
+ }
3296
+ /**
3297
+ * Tear down one element's effects and clear its library-owned attributes.
3298
+ *
3299
+ * @complexity O(c) time in retained cleanups; O(1) extra space.
3300
+ * @overallScore 100
3301
+ */
3302
+ release(el) {
3303
+ const state = this.states.get(el);
3304
+ if (!state) return;
3305
+ releaseDerived(this.port, el);
3306
+ const wasRunning = state.status === "running";
3307
+ state.cancelled = true;
3308
+ state.controller.abort();
3309
+ for (const instance of state.instances) runQuietly(() => instance.destroy());
3310
+ this.states.delete(el);
3311
+ this.liveElements.delete(el);
3312
+ this.gateWatcher?.unwatch(el);
3313
+ state.ledgers.restore();
3314
+ if (wasRunning) this.emit(el, state, KUI_EVENT.cancel, "reset");
3315
+ }
3316
+ /**
3317
+ * Tear down every tracked element inside a removed subtree.
3318
+ *
3319
+ * Scoped to `node`'s own descendants rather than re-scanning `liveElements` against the whole
3320
+ * page, so a removal event costs O(removed subtree), not O(every animated element alive
3321
+ * anywhere) — `dom-watcher.ts` can queue up to 100 removed roots per frame, and `liveElements`
3322
+ * only shrinks on release, so it stays large on a scroll-reveal-heavy page.
3323
+ *
3324
+ * Membership is checked against `liveElements` (the ground truth) rather than re-querying
3325
+ * `[${ATTR.source}]` the way `scan()` does: `dom-watcher.ts`'s `flush()` drains removed roots
3326
+ * before attribute-change roots, so if calling code strips `data-kui` and removes the element in
3327
+ * the same tick, a selector-based query would already miss it here and leak its teardown.
3328
+ *
3329
+ * `node` is typed `Element`, not `ParentNode`: `dom-watcher.ts`'s `onElementRemoved` — this
3330
+ * method's only caller — is itself typed `(el: Element) => void`, so there is no runtime case
3331
+ * where `node` is a `Document`/`DocumentFragment` to guard against.
3332
+ *
3333
+ * `restageAfterRemoval` runs over every candidate visited, not only the ones actually released:
3334
+ * a removed stagger member is exactly the case this closes, and whether it was "live" by
3335
+ * `liveElements`' definition is a question `GROUP_OF_CHILD` (in `stagger.ts`) answers on its own
3336
+ * terms. Without it, removing one item from a staggered group left every later sibling holding
3337
+ * the rank an index computed for a group one element larger — the stale-after-*edit* half of the
3338
+ * same defect `3d57ff7` already closed for an attribute change, left open for a removal.
3339
+ *
3340
+ * @complexity O(s) time in the removed subtree's element count; O(1) per candidate via the
3341
+ * `liveElements` Set lookup, plus stagger re-ranking bounded by the surviving groups' own sizes.
3342
+ * @overallScore 100
3343
+ */
3344
+ releaseTree(node) {
3345
+ const candidates = [node, ...node.querySelectorAll("*")];
3346
+ for (const el of candidates) {
3347
+ if (this.liveElements.has(el)) this.release(el);
3348
+ }
3349
+ restageAfterRemoval(candidates, this.reporter);
3350
+ restageAfterTreeRelease(this.port, candidates);
3351
+ }
3352
+ destroy() {
3353
+ this.domWatcher?.destroy();
3354
+ releaseStagger(this.root);
3355
+ for (const el of [...this.liveElements]) this.release(el);
3356
+ this.gateWatcher?.destroy();
3357
+ this.binder.destroy();
3358
+ this.scheduler.destroy();
3359
+ this.started = false;
3360
+ }
3361
+ /**
3362
+ * Start watching for DOM insertions, removals, and attribute changes.
3363
+ *
3364
+ * Attribute changes recompile in place; insertions scan; removals tear down so listeners and
3365
+ * observers do not outlive their elements.
3366
+ *
3367
+ * @complexity O(1) time and space to build and start; the watcher's own callback runs O(n) time
3368
+ * in the nodes one mutation record carries.
3369
+ * @overallScore 100
3370
+ */
3371
+ watch() {
3372
+ this.domWatcher ??= createDomWatcher({
3373
+ root: this.root,
3374
+ onElementAdded: (el) => {
3375
+ this.scan(el);
3376
+ adoptLateMatches(this.port, el);
3377
+ },
3378
+ onElementRemoved: (el) => this.releaseTree(el),
3379
+ onAttributeChanged: (el) => {
3380
+ this.process(el);
3381
+ restageAround(el, this.reporter);
3382
+ }
3383
+ });
3384
+ this.domWatcher.watch();
3385
+ }
3386
+ };
3387
+ function createAnimator(options = {}) {
3388
+ return new Animator(options);
3389
+ }
3390
+ function resolveCollaborators(options) {
3391
+ const root = options.root ?? globalThis.document;
3392
+ const capabilities = options.capabilities ?? detect();
3393
+ const reporter = options.reporter ?? silentReporter();
3394
+ const scheduler = options.scheduler ?? createScrollScheduler();
3395
+ const rootResolver = options.rootResolver ?? defaultRootResolver(root);
3396
+ const respectReducedMotion = (options.reducedMotion ?? "respect") === "respect";
3397
+ return {
3398
+ registry: options.registry ?? new Registry(),
3399
+ capabilities,
3400
+ root,
3401
+ reporter,
3402
+ binder: options.binder ?? createActivationBinder({ reporter }),
3403
+ scheduler,
3404
+ rootResolver,
3405
+ jsEffectPreparer: resolveJsEffectPreparer(options.jsEffectPreparer, {
3406
+ scheduler,
3407
+ rootResolver,
3408
+ capabilities,
3409
+ reporter,
3410
+ respectReducedMotion
3411
+ }),
3412
+ // Not defaulted here (unlike the other collaborators above): building the real watcher needs
3413
+ // `this.scan`/`this.process`/`this.releaseTree`, which don't exist yet inside this free
3414
+ // function. `Animator.watch()` builds it lazily instead, so nothing observes — and no
3415
+ // `MutationObserver` is ever constructed — unless `shouldObserve` is true and `start()` runs.
3416
+ domWatcher: options.domWatcher,
3417
+ respectReducedMotion,
3418
+ shouldObserve: options.observe ?? false
3419
+ };
3420
+ }
3421
+ function resolveJsEffectPreparer(provided, deps) {
3422
+ return provided ?? createJsEffectPreparer(deps);
3423
+ }
3424
+ function defaultRootResolver(root) {
3425
+ const doc = isElementNode(root) ? root.ownerDocument : root;
3426
+ const win = doc?.defaultView ?? globalThis;
3427
+ return createRootResolver({ win });
3428
+ }
3429
+ function isDirectional(instance) {
3430
+ return instance.play !== void 0 && instance.reverse !== void 0;
3431
+ }
3432
+ function runQuietly(cleanup) {
3433
+ try {
3434
+ cleanup();
3435
+ } catch {
3436
+ }
3437
+ }
3438
+
3439
+ export {
3440
+ detect,
3441
+ consoleReporter,
3442
+ silentReporter,
3443
+ collectingReporter,
3444
+ Animator,
3445
+ createAnimator
3446
+ };