kuinetic 0.1.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/esm/chunk-LWS4OSLX.mjs +730 -0
  4. package/dist/esm/chunk-QZIJ7WZI.mjs +3312 -0
  5. package/dist/esm/chunk-R3TGDJKA.mjs +1264 -0
  6. package/dist/esm/core/index.mjs +34 -0
  7. package/dist/esm/effects/index.mjs +27 -0
  8. package/dist/esm/index.mjs +52 -0
  9. package/dist/kuinetic.all.js +5291 -0
  10. package/dist/kuinetic.css +2338 -0
  11. package/dist/kuinetic.js +5281 -0
  12. package/dist/types/core/activation.d.ts +21 -0
  13. package/dist/types/core/animator.d.ts +216 -0
  14. package/dist/types/core/attrs.d.ts +15 -0
  15. package/dist/types/core/capabilities.d.ts +19 -0
  16. package/dist/types/core/channels.d.ts +25 -0
  17. package/dist/types/core/compile.d.ts +49 -0
  18. package/dist/types/core/dom-watcher.d.ts +35 -0
  19. package/dist/types/core/effect-context.d.ts +49 -0
  20. package/dist/types/core/element-config.d.ts +52 -0
  21. package/dist/types/core/flip.d.ts +80 -0
  22. package/dist/types/core/gesture.d.ts +99 -0
  23. package/dist/types/core/index.d.ts +14 -0
  24. package/dist/types/core/instances.d.ts +98 -0
  25. package/dist/types/core/js-effect-preparer.d.ts +46 -0
  26. package/dist/types/core/js-params.d.ts +124 -0
  27. package/dist/types/core/owned-styles.d.ts +44 -0
  28. package/dist/types/core/params.d.ts +65 -0
  29. package/dist/types/core/parse.d.ts +22 -0
  30. package/dist/types/core/path-morph.d.ts +83 -0
  31. package/dist/types/core/play.d.ts +48 -0
  32. package/dist/types/core/registry.d.ts +38 -0
  33. package/dist/types/core/reporter.d.ts +35 -0
  34. package/dist/types/core/scroll-scheduler.d.ts +153 -0
  35. package/dist/types/core/spring.d.ts +83 -0
  36. package/dist/types/core/stagger.d.ts +19 -0
  37. package/dist/types/core/style-plan.d.ts +56 -0
  38. package/dist/types/core/types.d.ts +239 -0
  39. package/dist/types/effects/catalog/ambient.d.ts +18 -0
  40. package/dist/types/effects/catalog/core.d.ts +19 -0
  41. package/dist/types/effects/catalog/feedback.d.ts +21 -0
  42. package/dist/types/effects/catalog/index.d.ts +21 -0
  43. package/dist/types/effects/catalog/interaction-shared.d.ts +56 -0
  44. package/dist/types/effects/catalog/interaction.d.ts +22 -0
  45. package/dist/types/effects/catalog/media.d.ts +13 -0
  46. package/dist/types/effects/catalog/numbers-shared.d.ts +103 -0
  47. package/dist/types/effects/catalog/numbers.d.ts +31 -0
  48. package/dist/types/effects/catalog/shared.d.ts +3 -0
  49. package/dist/types/effects/catalog/text-shared.d.ts +180 -0
  50. package/dist/types/effects/catalog/text.d.ts +17 -0
  51. package/dist/types/effects/forms/index.d.ts +14 -0
  52. package/dist/types/effects/forms/primitives.d.ts +32 -0
  53. package/dist/types/effects/gestures/index.d.ts +19 -0
  54. package/dist/types/effects/gestures/primitives.d.ts +2 -0
  55. package/dist/types/effects/index.d.ts +19 -0
  56. package/dist/types/effects/layout/index.d.ts +12 -0
  57. package/dist/types/effects/layout/presets.d.ts +9 -0
  58. package/dist/types/effects/layout/primitives.d.ts +2 -0
  59. package/dist/types/effects/navigation/index.d.ts +30 -0
  60. package/dist/types/effects/scroll-mechanics/index.d.ts +14 -0
  61. package/dist/types/effects/scroll-mechanics/presets.d.ts +6 -0
  62. package/dist/types/effects/scroll-mechanics/primitives.d.ts +2 -0
  63. package/dist/types/effects/scroll-mechanics/tracker.d.ts +55 -0
  64. package/dist/types/effects/shared.d.ts +26 -0
  65. package/dist/types/effects/svg/index.d.ts +13 -0
  66. package/dist/types/effects/three-d/index.d.ts +20 -0
  67. package/dist/types/index.d.ts +16 -0
  68. package/package.json +78 -0
@@ -0,0 +1,1264 @@
1
+ import {
2
+ Registry,
3
+ createAttributeLedger,
4
+ createCssInstance,
5
+ createRootResolver,
6
+ createScrollScheduler,
7
+ createStyleLedger,
8
+ readEffectParams,
9
+ readEffectTiming,
10
+ resolveParams,
11
+ suggest,
12
+ timingProperty
13
+ } from "./chunk-LWS4OSLX.mjs";
14
+
15
+ // src/core/attrs.ts
16
+ var ATTR = {
17
+ /** Authored. The rich grammar. */
18
+ source: "data-kui",
19
+ /** Library-owned and unstable: normalized effect names, for CSS hooks and debugging. */
20
+ normalized: "data-kui-fx",
21
+ state: "data-kui-state",
22
+ on: "data-kui-on",
23
+ timeline: "data-kui-timeline",
24
+ threshold: "data-kui-threshold",
25
+ stagger: "data-kui-stagger",
26
+ cloak: "data-kui-cloak",
27
+ /** Reduced-motion policy, stamped from the primitive so the CSS layer can act on it. */
28
+ rm: "data-kui-rm"
29
+ };
30
+
31
+ // src/core/element-config.ts
32
+ var ACTIVATIONS = /* @__PURE__ */ new Set(["load", "enter", "hover", "focus", "click", "manual"]);
33
+ var TIMELINES = /* @__PURE__ */ new Set(["time", "view", "scroll", "pointer"]);
34
+ function readAttributes(el) {
35
+ return {
36
+ source: el.getAttribute(ATTR.source) ?? "",
37
+ on: el.getAttribute(ATTR.on),
38
+ timeline: el.getAttribute(ATTR.timeline),
39
+ threshold: el.getAttribute(ATTR.threshold)
40
+ };
41
+ }
42
+ function resolveConfig(attributes, parsed) {
43
+ const rawTimeline = parsed.timeline ?? attributes.timeline ?? "time";
44
+ const [head = "time", ...rest] = rawTimeline.trim().split(/\s+/);
45
+ const authored = parsed.activation ?? readActivation(attributes.on);
46
+ return {
47
+ activation: authored ?? "enter",
48
+ activationAuthored: authored !== void 0,
49
+ timeline: TIMELINES.has(head) ? head : "time",
50
+ range: rest.join(" "),
51
+ threshold: parsed.threshold ?? attributes.threshold ?? "0%"
52
+ };
53
+ }
54
+ function readActivation(attribute) {
55
+ return attribute && ACTIVATIONS.has(attribute) ? attribute : void 0;
56
+ }
57
+ function toThresholdRatio(raw) {
58
+ const value = Number.parseFloat(raw);
59
+ if (Number.isNaN(value)) return 0;
60
+ const ratio = raw.includes("%") ? value / 100 : value;
61
+ return Math.min(1, Math.max(0, ratio));
62
+ }
63
+
64
+ // src/core/activation.ts
65
+ var NOOP = () => {
66
+ };
67
+ var ACTIVATION_EVENTS = {
68
+ load: [],
69
+ enter: [],
70
+ manual: [],
71
+ hover: ["pointerenter", "focusin"],
72
+ focus: ["focusin"],
73
+ click: ["click"]
74
+ };
75
+ function createActivationBinder(options = {}) {
76
+ const observers = /* @__PURE__ */ new Map();
77
+ const callbacks = /* @__PURE__ */ new WeakMap();
78
+ const createObserver = options.createObserver ?? defaultObserverFactory();
79
+ function observerFor(threshold) {
80
+ if (!createObserver) return void 0;
81
+ const ratio = toThresholdRatio(threshold);
82
+ const key = String(ratio);
83
+ const existing = observers.get(key);
84
+ if (existing) return { key, shared: existing };
85
+ const observer = createObserver(
86
+ (entries) => {
87
+ for (const entry of entries) {
88
+ if (!entry.isIntersecting) continue;
89
+ const binding = callbacks.get(entry.target);
90
+ binding?.activate();
91
+ binding?.release();
92
+ }
93
+ },
94
+ { threshold: ratio }
95
+ );
96
+ const shared = { observer, count: 0 };
97
+ observers.set(key, shared);
98
+ return { key, shared };
99
+ }
100
+ function bindObserved(el, threshold, onActivate) {
101
+ const binding = observerFor(threshold);
102
+ if (!binding) {
103
+ onActivate();
104
+ return NOOP;
105
+ }
106
+ const { key, shared } = binding;
107
+ let active = true;
108
+ const release = () => {
109
+ if (!active) return;
110
+ active = false;
111
+ callbacks.delete(el);
112
+ shared.observer.unobserve(el);
113
+ shared.count--;
114
+ if (shared.count > 0) return;
115
+ shared.observer.disconnect();
116
+ observers.delete(key);
117
+ };
118
+ callbacks.set(el, { activate: onActivate, release });
119
+ shared.count++;
120
+ shared.observer.observe(el);
121
+ return release;
122
+ }
123
+ function bindEvents(el, activation, onActivate) {
124
+ const types = ACTIVATION_EVENTS[activation];
125
+ if (types.length === 0) return NOOP;
126
+ const handler = () => onActivate();
127
+ for (const type of types) el.addEventListener(type, handler, { passive: true });
128
+ return () => {
129
+ for (const type of types) el.removeEventListener(type, handler);
130
+ };
131
+ }
132
+ return {
133
+ bind(el, activation, threshold, onActivate) {
134
+ if (activation === "load") {
135
+ onActivate();
136
+ return NOOP;
137
+ }
138
+ if (activation === "enter") return bindObserved(el, threshold, onActivate);
139
+ return bindEvents(el, activation, onActivate);
140
+ },
141
+ destroy() {
142
+ for (const { observer } of observers.values()) observer.disconnect();
143
+ observers.clear();
144
+ }
145
+ };
146
+ }
147
+ function defaultObserverFactory() {
148
+ if (typeof IntersectionObserver === "undefined") return void 0;
149
+ return (callback, init) => new IntersectionObserver(callback, init);
150
+ }
151
+
152
+ // src/core/capabilities.ts
153
+ function supports(property, value) {
154
+ if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
155
+ try {
156
+ return CSS.supports(property, value);
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+ var cached;
162
+ function detect(force = false) {
163
+ if (cached && !force) return cached;
164
+ cached = {
165
+ viewTimeline: supports("animation-timeline", "view()"),
166
+ scrollTimeline: supports("animation-timeline", "scroll()"),
167
+ animationRange: supports("animation-range", "entry 0% cover 30%"),
168
+ // `translate`/`rotate`/`scale` as independent properties is what makes the channel model
169
+ // possible at all — under the `transform` shorthand every effect would collide.
170
+ individualTransforms: supports("translate", "0 10px") && supports("scale", "1.1"),
171
+ scrollTimelineName: supports("scroll-timeline-name", "--x"),
172
+ viewTransitions: typeof document !== "undefined" && "startViewTransition" in document,
173
+ intersectionObserver: typeof IntersectionObserver !== "undefined",
174
+ reducedMotion: typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches
175
+ };
176
+ return cached;
177
+ }
178
+
179
+ // src/core/channels.ts
180
+ function findConflicts(claims) {
181
+ const conflicts = [];
182
+ const seen = /* @__PURE__ */ new Map();
183
+ for (const claim of claims) {
184
+ for (const channel of claim.channels) {
185
+ const owner = seen.get(channel);
186
+ if (owner === void 0) {
187
+ seen.set(channel, claim.name);
188
+ continue;
189
+ }
190
+ conflicts.push({ channel, effects: [owner, claim.name] });
191
+ }
192
+ }
193
+ return conflicts;
194
+ }
195
+ function describeConflicts(conflicts) {
196
+ return conflicts.map((c) => `"${c.effects[0]}" and "${c.effects[1]}" both animate ${c.channel}`).join("; ");
197
+ }
198
+
199
+ // src/core/compile.ts
200
+ var NATIVE_EASINGS = /* @__PURE__ */ new Set([
201
+ "linear",
202
+ "ease",
203
+ "ease-in",
204
+ "ease-out",
205
+ "ease-in-out",
206
+ "step-start",
207
+ "step-end"
208
+ ]);
209
+ var RM_RANK = { shorten: 0, crossfade: 1, disable: 2 };
210
+ function compile(parsed, registry, timeline) {
211
+ const warnings = [...parsed.warnings];
212
+ const { entries, unknown } = resolveEntries(parsed.specs, registry, warnings);
213
+ if (entries.length === 0) {
214
+ return emptyPlan(unknown, warnings);
215
+ }
216
+ const composed = resolveComposition(entries, registry, warnings);
217
+ return buildPlan(composed, timeline, unknown, warnings);
218
+ }
219
+ function emptyPlan(unknown, warnings) {
220
+ return {
221
+ fxNames: [],
222
+ vars: {},
223
+ declarations: {},
224
+ jsEffects: [],
225
+ unknown,
226
+ reducedMotion: "shorten",
227
+ supportedActivations: [],
228
+ supportedTimelines: [],
229
+ channels: [],
230
+ warnings
231
+ };
232
+ }
233
+ function resolveEntries(specs, registry, warnings) {
234
+ const entries = [];
235
+ const unknown = [];
236
+ for (const spec of specs) {
237
+ const resolved = registry.resolve(spec.name);
238
+ if (resolved) {
239
+ entries.push({ spec, resolved });
240
+ continue;
241
+ }
242
+ unknown.push(spec.name);
243
+ const hint = suggest(spec.name, registry.names());
244
+ const suffix = hint ? ` \u2014 did you mean "${hint}"?` : "";
245
+ warnings.push(`unknown effect "${spec.name}"${suffix}`);
246
+ }
247
+ return { entries, unknown };
248
+ }
249
+ function resolveComposition(entries, registry, warnings) {
250
+ if (entries.length <= 1) return entries;
251
+ const conflicts = findConflicts(
252
+ entries.map((e) => ({ name: e.spec.name, channels: e.resolved.primitive.channels }))
253
+ );
254
+ if (conflicts.length === 0) return entries;
255
+ const combo = registry.findCombo(entries.map((e) => e.spec.name));
256
+ const remedy = combo ? `Use the "${combo.preset.name}" effect instead.` : "Apply them to nested elements, or register a combined effect.";
257
+ warnings.push(`cannot compose: ${describeConflicts(conflicts)}. ${remedy}`);
258
+ return [entries[0]];
259
+ }
260
+ function buildPlan(entries, timeline, unknown, warnings) {
261
+ const plan = emptyPlan(unknown, warnings);
262
+ const tracks = {
263
+ names: [],
264
+ durations: [],
265
+ delays: [],
266
+ easings: [],
267
+ iterationCounts: []
268
+ };
269
+ const channels = /* @__PURE__ */ new Set();
270
+ let activations;
271
+ let timelines;
272
+ for (const entry of entries) {
273
+ const { preset, primitive } = entry.resolved;
274
+ plan.fxNames.push(preset.name);
275
+ plan.reducedMotion = strictestPolicy(plan.reducedMotion, primitive.reducedMotion);
276
+ plan.defaultActivation ??= primitive.defaultActivation;
277
+ activations = intersect(activations, primitive.supportedActivations);
278
+ timelines = intersect(timelines, primitive.supportedTimelines);
279
+ for (const channel of primitive.channels) channels.add(channel);
280
+ warnUnsupportedTimeline(preset.name, primitive.supportedTimelines, timeline, warnings);
281
+ Object.assign(
282
+ plan.vars,
283
+ resolveParams(entry.spec.params, primitive.parameters, (m) => warnings.push(m))
284
+ );
285
+ if (primitive.renderer === "css-keyframes") pushTrack(tracks, entry);
286
+ else plan.jsEffects.push(entry);
287
+ }
288
+ Object.assign(plan.declarations, declarationsFor(tracks));
289
+ plan.supportedActivations = activations;
290
+ plan.supportedTimelines = timelines;
291
+ plan.channels = [...channels];
292
+ return plan;
293
+ }
294
+ function intersect(accumulated, supported) {
295
+ if (!accumulated) return [...supported];
296
+ return accumulated.filter((value) => supported.includes(value));
297
+ }
298
+ function iterationCountProperty(presetName) {
299
+ return `--kui-fx-${presetName}-iterations`;
300
+ }
301
+ function pushTrack(tracks, entry) {
302
+ const { spec, resolved } = entry;
303
+ const id = resolved.primitive.id;
304
+ tracks.names.push(resolved.preset.keyframes ?? `kui-${resolved.preset.name}`);
305
+ tracks.durations.push(spec.duration ?? `var(${timingProperty(id, "duration")}, 600ms)`);
306
+ tracks.delays.push(staggerDelay(spec.delay, id));
307
+ tracks.easings.push(easingValue(spec.easing, id));
308
+ tracks.iterationCounts.push(`var(${iterationCountProperty(resolved.preset.name)}, 1)`);
309
+ }
310
+ function declarationsFor(tracks) {
311
+ if (tracks.names.length === 0) return {};
312
+ return {
313
+ "animation-name": tracks.names.join(", "),
314
+ "animation-duration": tracks.durations.join(", "),
315
+ "animation-delay": tracks.delays.join(", "),
316
+ "animation-timing-function": tracks.easings.join(", "),
317
+ "animation-iteration-count": tracks.iterationCounts.join(", "),
318
+ "animation-fill-mode": tracks.names.map(() => "both").join(", ")
319
+ };
320
+ }
321
+ function strictestPolicy(a, b) {
322
+ return RM_RANK[b] > RM_RANK[a] ? b : a;
323
+ }
324
+ function warnUnsupportedTimeline(name, supported, timeline, warnings) {
325
+ if (supported.includes(timeline)) return;
326
+ warnings.push(
327
+ `"${name}" does not support timeline "${timeline}" (supports: ${supported.join(", ")})`
328
+ );
329
+ }
330
+ function staggerDelay(delay, primitiveId) {
331
+ const base = delay ?? `var(${timingProperty(primitiveId, "delay")}, 0ms)`;
332
+ return `calc(${base} + var(--kui-i, 0) * var(--kui-stagger, 0ms))`;
333
+ }
334
+ function easingValue(easing, primitiveId) {
335
+ if (!easing) return `var(${timingProperty(primitiveId, "ease")}, ease-out)`;
336
+ if (NATIVE_EASINGS.has(easing)) return easing;
337
+ if (easing.includes("(")) return easing;
338
+ return `var(--kui-ease-${easing}, ease-out)`;
339
+ }
340
+
341
+ // src/core/dom-watcher.ts
342
+ var WORK_BUDGET = 100;
343
+ function createDomWatcher(options) {
344
+ const { root, onElementAdded, onElementRemoved, onAttributeChanged } = options;
345
+ const schedule = options.schedule ?? scheduleFrame;
346
+ const createObserver = options.createObserver ?? defaultObserverFactory2();
347
+ const added = /* @__PURE__ */ new Set();
348
+ const removed = /* @__PURE__ */ new Set();
349
+ const changed = /* @__PURE__ */ new Set();
350
+ let observer;
351
+ let scheduled = false;
352
+ let destroyed = false;
353
+ function collect(record) {
354
+ if (record.type === "attributes") {
355
+ const target = asElement(record.target);
356
+ if (target) changed.add(target);
357
+ return;
358
+ }
359
+ for (const node of record.addedNodes) queueRoot(added, asElement(node));
360
+ for (const node of record.removedNodes) queueRoot(removed, asElement(node));
361
+ }
362
+ function queueWork(records) {
363
+ for (const record of records) collect(record);
364
+ if (scheduled) return;
365
+ scheduled = true;
366
+ schedule(flush);
367
+ }
368
+ function flush() {
369
+ scheduled = false;
370
+ if (destroyed) return;
371
+ let remaining = WORK_BUDGET;
372
+ remaining = drain(removed, onElementRemoved, remaining);
373
+ remaining = drain(added, onElementAdded, remaining);
374
+ drain(changed, onAttributeChanged, remaining);
375
+ if (added.size + removed.size + changed.size > 0) {
376
+ scheduled = true;
377
+ schedule(flush);
378
+ }
379
+ }
380
+ return {
381
+ watch() {
382
+ if (!createObserver) return;
383
+ destroyed = false;
384
+ observer = createObserver(queueWork);
385
+ observer.observe(root, {
386
+ subtree: true,
387
+ childList: true,
388
+ attributes: true,
389
+ attributeFilter: [ATTR.source, ATTR.on, ATTR.timeline, ATTR.threshold]
390
+ });
391
+ },
392
+ destroy() {
393
+ destroyed = true;
394
+ observer?.disconnect();
395
+ added.clear();
396
+ removed.clear();
397
+ changed.clear();
398
+ }
399
+ };
400
+ }
401
+ function queueRoot(roots, candidate) {
402
+ if (!candidate) return;
403
+ for (const root of roots) {
404
+ if (root.contains(candidate)) return;
405
+ if (candidate.contains(root)) roots.delete(root);
406
+ }
407
+ roots.add(candidate);
408
+ }
409
+ function drain(roots, callback, budget) {
410
+ for (const root of roots) {
411
+ if (budget === 0) break;
412
+ roots.delete(root);
413
+ callback(root);
414
+ budget--;
415
+ }
416
+ return budget;
417
+ }
418
+ function asElement(node) {
419
+ return node.nodeType === 1 ? node : null;
420
+ }
421
+ function scheduleFrame(callback) {
422
+ const requestFrame = globalThis.requestAnimationFrame;
423
+ if (typeof requestFrame === "function") requestFrame(() => callback());
424
+ else queueMicrotask(callback);
425
+ }
426
+ function defaultObserverFactory2() {
427
+ if (typeof MutationObserver === "undefined") return void 0;
428
+ return (callback) => new MutationObserver(callback);
429
+ }
430
+
431
+ // src/core/js-effect-preparer.ts
432
+ function createJsEffectPreparer(options) {
433
+ const { scheduler, rootResolver, capabilities, reporter, respectReducedMotion } = options;
434
+ function contextFor(el, signal, ledger) {
435
+ const doc = el.ownerDocument;
436
+ return {
437
+ doc,
438
+ win: doc.defaultView ?? globalThis,
439
+ scheduler,
440
+ rootFor: rootResolver,
441
+ capabilities,
442
+ invalidate: () => scheduler.invalidate(),
443
+ warn: (message) => reporter.warn(message, el),
444
+ reducedMotion: respectReducedMotion && capabilities.reducedMotion,
445
+ signal,
446
+ style: ledger
447
+ };
448
+ }
449
+ return {
450
+ prepare({ el, plan, signal, ledger }) {
451
+ const instances = [];
452
+ if (plan.jsEffects.length === 0) return instances;
453
+ const ctx = contextFor(el, signal, ledger);
454
+ for (const { spec, resolved } of plan.jsEffects) {
455
+ const prepare = resolved.primitive.prepare;
456
+ if (!prepare) continue;
457
+ const warn = (message) => reporter.warn(message, el);
458
+ const params = readEffectParams(
459
+ { ...resolved.preset.params, ...spec.params },
460
+ resolved.primitive.parameters,
461
+ warn,
462
+ readEffectTiming(spec, warn)
463
+ );
464
+ try {
465
+ instances.push(prepare(el, params, ctx));
466
+ } catch (error) {
467
+ reporter.warn(`"${spec.name}" failed to initialise: ${String(error)}`, el);
468
+ }
469
+ }
470
+ return instances;
471
+ }
472
+ };
473
+ }
474
+
475
+ // src/core/parse.ts
476
+ var TIME_RE = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:ms|s)$/;
477
+ var EASING_FUNCTIONS = ["cubic-bezier(", "steps(", "linear("];
478
+ var ACTIVATIONS2 = /* @__PURE__ */ new Set([
479
+ "load",
480
+ "enter",
481
+ "hover",
482
+ "focus",
483
+ "click",
484
+ "manual"
485
+ ]);
486
+ var EASING_KEYWORDS = /* @__PURE__ */ new Set([
487
+ "linear",
488
+ "ease",
489
+ "ease-in",
490
+ "ease-out",
491
+ "ease-in-out",
492
+ "step-start",
493
+ "step-end",
494
+ "expo-in",
495
+ "expo-out",
496
+ "expo-in-out",
497
+ "back-in",
498
+ "back-out",
499
+ "back-in-out",
500
+ "quart-out",
501
+ "circ-out",
502
+ "spring"
503
+ ]);
504
+ function splitTopLevel(input, delimiter, warnings = []) {
505
+ const parts = [];
506
+ const scanner = { depth: 0, quote: null, escaped: false };
507
+ let buffer = "";
508
+ for (const char of input) {
509
+ if (isSeparator(char, delimiter, scanner)) {
510
+ if (buffer.trim()) parts.push(buffer.trim());
511
+ buffer = "";
512
+ continue;
513
+ }
514
+ buffer += char;
515
+ }
516
+ if (buffer.trim()) parts.push(buffer.trim());
517
+ if (scanner.quote) warnings.push(`unterminated ${scanner.quote} quote in "${input}"`);
518
+ else if (scanner.depth > 0) warnings.push(`unclosed "(" in "${input}"`);
519
+ return parts;
520
+ }
521
+ function isSeparator(char, delimiter, scanner) {
522
+ if (scanner.quote) return advanceQuote(char, scanner);
523
+ if (char === '"' || char === "'") {
524
+ scanner.quote = char;
525
+ return false;
526
+ }
527
+ if (char === "(") scanner.depth++;
528
+ else if (char === ")") scanner.depth = Math.max(0, scanner.depth - 1);
529
+ if (scanner.depth !== 0) return false;
530
+ return delimiter === " " ? /\s/.test(char) : char === delimiter;
531
+ }
532
+ function advanceQuote(char, scanner) {
533
+ if (scanner.escaped) scanner.escaped = false;
534
+ else if (char === "\\") scanner.escaped = true;
535
+ else if (char === scanner.quote) scanner.quote = null;
536
+ return false;
537
+ }
538
+ function splitPair(token) {
539
+ let depth = 0;
540
+ for (let i = 0; i < token.length; i++) {
541
+ const char = token[i];
542
+ if (char === "(") depth++;
543
+ else if (char === ")") depth = Math.max(0, depth - 1);
544
+ else if (char === ":" && depth === 0) {
545
+ const key = token.slice(0, i).trim();
546
+ const value = unquote(token.slice(i + 1).trim());
547
+ return key && value ? [key, value] : null;
548
+ }
549
+ }
550
+ return null;
551
+ }
552
+ function unquote(value) {
553
+ const first = value[0];
554
+ if ((first === '"' || first === "'") && value.endsWith(first) && value.length > 1) {
555
+ return value.slice(1, -1).replaceAll(`\\${first}`, first);
556
+ }
557
+ return value;
558
+ }
559
+ function classify(token) {
560
+ const pair = splitPair(token);
561
+ if (pair) return { kind: "pair", key: pair[0], value: pair[1] };
562
+ if (TIME_RE.test(token)) return { kind: "time", value: token };
563
+ if (isEasing(token)) return { kind: "easing", value: token };
564
+ return { kind: "unknown", value: token };
565
+ }
566
+ function isEasing(token) {
567
+ if (EASING_KEYWORDS.has(token)) return true;
568
+ return EASING_FUNCTIONS.some((fn) => token.startsWith(fn) && token.endsWith(")"));
569
+ }
570
+ function parse(input) {
571
+ const result = { specs: [], warnings: [] };
572
+ for (const segment of splitTopLevel(input ?? "", ",", result.warnings)) {
573
+ const spec = parseSegment(segment, result);
574
+ if (spec) result.specs.push(spec);
575
+ }
576
+ return result;
577
+ }
578
+ function parseSegment(segment, result) {
579
+ const tokens = splitTopLevel(segment, " ", result.warnings);
580
+ const name = tokens.shift();
581
+ if (splitPair(name)) {
582
+ result.warnings.push(`effect name expected, got "${name}"`);
583
+ return null;
584
+ }
585
+ const spec = { name, params: {} };
586
+ let timeCount = 0;
587
+ for (const raw of tokens) {
588
+ const token = classify(raw);
589
+ if (token.kind === "time") timeCount = applyTime(spec, token.value, timeCount, result.warnings);
590
+ else applyToken(token, spec, segment, result);
591
+ }
592
+ return spec;
593
+ }
594
+ function applyTime(spec, value, seen, warnings) {
595
+ if (seen === 0) spec.duration = value;
596
+ else if (seen === 1) spec.delay = value;
597
+ else warnings.push(`third time value "${value}" ignored (expected duration then delay)`);
598
+ return seen + 1;
599
+ }
600
+ function applyToken(token, spec, segment, result) {
601
+ if (token.kind === "easing") {
602
+ if (spec.easing) result.warnings.push(`duplicate easing "${token.value}" in "${segment}"`);
603
+ spec.easing = token.value;
604
+ return;
605
+ }
606
+ if (token.kind === "unknown") {
607
+ result.warnings.push(
608
+ `unrecognised token "${token.value}" in "${segment}" \u2014 expected [duration] [delay] [easing] or key:value`
609
+ );
610
+ return;
611
+ }
612
+ if (Object.hasOwn(HOISTS, token.key)) {
613
+ HOISTS[token.key](result, token.value);
614
+ return;
615
+ }
616
+ if (token.key in spec.params) {
617
+ result.warnings.push(`duplicate parameter "${token.key}" in "${segment}"`);
618
+ }
619
+ spec.params[token.key] = token.value;
620
+ }
621
+ var HOISTS = {
622
+ on(result, value) {
623
+ if (!ACTIVATIONS2.has(value)) {
624
+ result.warnings.push(`unknown activation "${value}"`);
625
+ return;
626
+ }
627
+ assignOnce(result, "activation", value, "activations");
628
+ },
629
+ timeline(result, value) {
630
+ assignOnce(result, "timeline", value, "timelines");
631
+ },
632
+ threshold(result, value) {
633
+ assignOnce(result, "threshold", value, "thresholds");
634
+ }
635
+ };
636
+ function assignOnce(result, key, value, label) {
637
+ const current = result[key];
638
+ if (current === void 0) {
639
+ result[key] = value;
640
+ return;
641
+ }
642
+ if (current !== value) {
643
+ result.warnings.push(`conflicting ${label} "${String(current)}" and "${String(value)}"`);
644
+ }
645
+ }
646
+
647
+ // src/core/play.ts
648
+ function resolveTargets(target, root) {
649
+ if (typeof target === "string") return [...root.querySelectorAll(target)];
650
+ if (target instanceof Element) return [target];
651
+ return [...target];
652
+ }
653
+ function time(value) {
654
+ if (value === void 0) return void 0;
655
+ return typeof value === "number" ? `${value}ms` : value;
656
+ }
657
+ var STRUCTURAL_RE = /[\s,()"']/;
658
+ function quoteIfNeeded(value) {
659
+ if (!STRUCTURAL_RE.test(value)) return value;
660
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
661
+ return `"${escaped}"`;
662
+ }
663
+ function hasTopLevelColon(value) {
664
+ let depth = 0;
665
+ for (const char of value) {
666
+ if (char === "(") depth++;
667
+ else if (char === ")") depth = Math.max(0, depth - 1);
668
+ else if (char === ":" && depth === 0) return true;
669
+ }
670
+ return false;
671
+ }
672
+ function assertBareToken(label, value) {
673
+ const warnings = [];
674
+ const bySpace = splitTopLevel(value, " ", warnings);
675
+ const byComma = splitTopLevel(value, ",", warnings);
676
+ const isSingleToken = bySpace.length === 1 && bySpace[0] === value;
677
+ const safe = warnings.length === 0 && isSingleToken && byComma.length === 1 && !hasTopLevelColon(value);
678
+ if (!safe) {
679
+ throw new Error(
680
+ `play(): ${label} "${value}" cannot be serialized \u2014 it contains a space, comma, colon, quote, or unbalanced parenthesis the parser cannot read back as one token`
681
+ );
682
+ }
683
+ }
684
+ function toAttributeValue(effect, options = {}) {
685
+ assertBareToken("effect", effect);
686
+ const { duration, delay, ease, ...rest } = options;
687
+ delete rest.stagger;
688
+ const parts = [effect];
689
+ const resolvedDelay = time(delay);
690
+ const resolvedDuration = time(duration) ?? (resolvedDelay ? "0ms" : void 0);
691
+ if (resolvedDuration) {
692
+ assertBareToken("duration", resolvedDuration);
693
+ parts.push(resolvedDuration);
694
+ }
695
+ if (resolvedDelay) {
696
+ assertBareToken("delay", resolvedDelay);
697
+ parts.push(resolvedDelay);
698
+ }
699
+ if (ease) {
700
+ assertBareToken("easing", ease);
701
+ parts.push(ease);
702
+ }
703
+ for (const [key, value] of Object.entries(rest)) {
704
+ if (value === void 0) continue;
705
+ assertBareToken("parameter name", key);
706
+ parts.push(`${key}:${quoteIfNeeded(String(value))}`);
707
+ }
708
+ return parts.join(" ");
709
+ }
710
+ function play(request, options = {}) {
711
+ const { animator, root, target, effect } = request;
712
+ const elements = resolveTargets(target, root);
713
+ const stagger = time(options.stagger);
714
+ const source = toAttributeValue(effect, options);
715
+ for (const [index, el] of elements.entries()) {
716
+ if (stagger) {
717
+ ;
718
+ el.style.setProperty("--kui-stagger", stagger);
719
+ el.style.setProperty("--kui-i", String(index));
720
+ }
721
+ animator.reset(el);
722
+ if (!el.hasAttribute(ATTR.on)) el.setAttribute(ATTR.on, "manual");
723
+ el.setAttribute(ATTR.source, source);
724
+ animator.process(el);
725
+ animator.activate(el);
726
+ }
727
+ const instancesOf = (el) => animator.stateOf(el)?.instances ?? [];
728
+ const finished = Promise.all(
729
+ elements.flatMap((el) => instancesOf(el).map((instance) => instance.finished))
730
+ ).then(() => void 0);
731
+ return {
732
+ elements,
733
+ finished,
734
+ cancel() {
735
+ for (const el of elements) for (const instance of instancesOf(el)) instance.cancel();
736
+ },
737
+ finish() {
738
+ for (const el of elements) for (const instance of instancesOf(el)) instance.finish();
739
+ }
740
+ };
741
+ }
742
+
743
+ // src/core/reporter.ts
744
+ function consoleReporter() {
745
+ return {
746
+ warn(message, subject) {
747
+ if (subject === void 0) console.warn(`[kuinetic] ${message}`);
748
+ else console.warn(`[kuinetic] ${message}`, subject);
749
+ }
750
+ };
751
+ }
752
+ function silentReporter() {
753
+ return { warn() {
754
+ } };
755
+ }
756
+ function collectingReporter() {
757
+ const messages = [];
758
+ return {
759
+ messages,
760
+ warn(message) {
761
+ messages.push(message);
762
+ }
763
+ };
764
+ }
765
+
766
+ // src/core/stagger.ts
767
+ function indexStaggerGroup(group) {
768
+ const step = group.getAttribute(ATTR.stagger);
769
+ if (step) group.style.setProperty("--kui-stagger", step);
770
+ let index = 0;
771
+ for (const child of group.children) {
772
+ if (child.hasAttribute(ATTR.source)) {
773
+ ;
774
+ child.style.setProperty("--kui-i", String(index));
775
+ index++;
776
+ }
777
+ }
778
+ }
779
+ function applyStagger(root) {
780
+ const selector = `[${ATTR.stagger}]`;
781
+ if (root instanceof Element && root.matches(selector)) indexStaggerGroup(root);
782
+ for (const group of root.querySelectorAll(selector)) indexStaggerGroup(group);
783
+ }
784
+
785
+ // src/core/style-plan.ts
786
+ function planStyles(input) {
787
+ const { plan, config, capabilities, respectReducedMotion } = input;
788
+ const reduce = respectReducedMotion && capabilities.reducedMotion;
789
+ const useNativeTimeline = supportsTimeline(config.timeline, capabilities) && plan.supportedTimelines.includes(config.timeline);
790
+ const properties = { ...plan.vars, ...plan.declarations };
791
+ Object.assign(properties, timelineProperties(config, capabilities, useNativeTimeline));
792
+ const hasCssAnimation = Object.keys(plan.declarations).length > 0;
793
+ const gate = resolveGate({
794
+ useNativeTimeline,
795
+ reduce,
796
+ activation: config.activation,
797
+ // JS effects are gated too. They emit no `animation` declaration, so only the play-state
798
+ // write is skipped — the activation itself still has to be bound, or `on:enter` and
799
+ // `on:click` would silently do nothing for every pinned, dragged, or morphing element.
800
+ hasWork: hasCssAnimation || plan.jsEffects.length > 0,
801
+ hasCssAnimation,
802
+ // A browser lacking standalone translate/rotate/scale support silently ignores any
803
+ // `@keyframes` step written in those properties — the animation never visibly completes. An
804
+ // effect deferred on that promise would sit paused (or, for an entrance reveal, invisible)
805
+ // forever, so it must reach its final state immediately instead, the same fail-open rule
806
+ // already applied under reduced motion.
807
+ unsupportedTransform: needsIndividualTransforms(plan.channels, capabilities)
808
+ });
809
+ if (gate === "deferred") properties["animation-play-state"] = "paused";
810
+ return {
811
+ properties,
812
+ attributes: {
813
+ [ATTR.normalized]: plan.fxNames.join(" "),
814
+ [ATTR.rm]: plan.reducedMotion,
815
+ [ATTR.state]: "ready"
816
+ },
817
+ gate,
818
+ activation: gate === "deferred" ? effectiveActivation(config) : null
819
+ };
820
+ }
821
+ function timelineProperties(config, capabilities, useNativeTimeline) {
822
+ if (!useNativeTimeline) return {};
823
+ const properties = {
824
+ "animation-timeline": config.timeline === "scroll" ? "scroll()" : "view()"
825
+ };
826
+ if (capabilities.animationRange) {
827
+ properties["animation-range"] = config.range || "entry 0% cover 60%";
828
+ }
829
+ return properties;
830
+ }
831
+ function resolveGate(input) {
832
+ if (input.useNativeTimeline) return "native-timeline";
833
+ if (!input.hasWork) return "immediate";
834
+ if (input.reduce || input.activation === "load" || input.unsupportedTransform) return "immediate";
835
+ return "deferred";
836
+ }
837
+ function needsIndividualTransforms(channels, capabilities) {
838
+ if (capabilities.individualTransforms) return false;
839
+ return channels.some((c) => c === "translate" || c === "rotate" || c === "scale");
840
+ }
841
+ function supportsTimeline(timeline, capabilities) {
842
+ if (timeline === "view") return capabilities.viewTimeline;
843
+ if (timeline === "scroll") return capabilities.scrollTimeline;
844
+ return false;
845
+ }
846
+ function effectiveActivation(config) {
847
+ if (config.timeline !== "time" && config.activation === "manual") return "enter";
848
+ return config.activation;
849
+ }
850
+ function applyStylePlan(request) {
851
+ const { plan, ledger, attributes } = request;
852
+ for (const [property, value] of Object.entries(plan.properties)) ledger.set(property, value);
853
+ for (const [attribute, value] of Object.entries(plan.attributes)) attributes.set(attribute, value);
854
+ ledger.claim("animation-play-state");
855
+ }
856
+
857
+ // src/core/animator.ts
858
+ var CLOAK_WATCHDOG_MS = 3e3;
859
+ function fingerprintOf(attributes) {
860
+ return [attributes.source, attributes.on, attributes.timeline, attributes.threshold].join("\0");
861
+ }
862
+ function isElementNode(node) {
863
+ return typeof Element !== "undefined" && node instanceof Element;
864
+ }
865
+ var Animator = class {
866
+ registry;
867
+ capabilities;
868
+ root;
869
+ reporter;
870
+ binder;
871
+ scheduler;
872
+ rootResolver;
873
+ jsEffectPreparer;
874
+ respectReducedMotion;
875
+ shouldObserve;
876
+ /** Runtime truth. Attributes are for CSS and debugging; they make a poor state machine. */
877
+ states = /* @__PURE__ */ new WeakMap();
878
+ /** Iterable lifecycle index; the WeakMap remains the fast state lookup. */
879
+ liveElements = /* @__PURE__ */ new Set();
880
+ /** Built lazily by `watch()` when not injected, so nothing observes until `start()` needs it. */
881
+ domWatcher;
882
+ started = false;
883
+ constructor(options = {}) {
884
+ const resolved = resolveCollaborators(options);
885
+ this.registry = resolved.registry;
886
+ this.capabilities = resolved.capabilities;
887
+ this.root = resolved.root;
888
+ this.reporter = resolved.reporter;
889
+ this.binder = resolved.binder;
890
+ this.scheduler = resolved.scheduler;
891
+ this.rootResolver = resolved.rootResolver;
892
+ this.jsEffectPreparer = resolved.jsEffectPreparer;
893
+ this.domWatcher = resolved.domWatcher;
894
+ this.respectReducedMotion = resolved.respectReducedMotion;
895
+ this.shouldObserve = resolved.shouldObserve;
896
+ }
897
+ /**
898
+ * Explicit entry point. Importing the library never touches the document, which keeps SSR,
899
+ * hydration, and tests deterministic.
900
+ *
901
+ * @complexity O(n) time in the number of elements scanned.
902
+ * @overallScore 100
903
+ */
904
+ start() {
905
+ if (this.started) return this;
906
+ this.started = true;
907
+ const watchdog = globalThis.setTimeout(() => this.uncloak(), CLOAK_WATCHDOG_MS);
908
+ try {
909
+ this.scan(this.root);
910
+ if (this.shouldObserve) this.watch();
911
+ } finally {
912
+ globalThis.clearTimeout(watchdog);
913
+ this.uncloak();
914
+ }
915
+ return this;
916
+ }
917
+ /**
918
+ * Process every unprocessed element in a subtree, including the root.
919
+ *
920
+ * `querySelectorAll` excludes the root, but an inserted subtree very often carries the
921
+ * attribute on its top node — skipping it silently drops those animations.
922
+ *
923
+ * @param root - Subtree to scan. Defaults to the animator's root.
924
+ * @complexity O(n) time in the subtree size; O(1) extra space.
925
+ * @overallScore 100
926
+ */
927
+ scan(root = this.root) {
928
+ if (!root) return this;
929
+ const selector = `[${ATTR.source}]`;
930
+ if (isElementNode(root) && root.matches(selector)) this.process(root);
931
+ for (const el of root.querySelectorAll(selector)) this.process(el);
932
+ applyStagger(root);
933
+ return this;
934
+ }
935
+ /**
936
+ * Compile and install one element's effects, recompiling if its attribute changed.
937
+ *
938
+ * @param el - Element carrying `data-kui`.
939
+ * @complexity O(e) time in the number of composed effects; O(e) space for the plan.
940
+ * @overallScore 100
941
+ */
942
+ process(el) {
943
+ const attributes = readAttributes(el);
944
+ const fingerprint = fingerprintOf(attributes);
945
+ const existing = this.states.get(el);
946
+ if (existing?.fingerprint === fingerprint) return;
947
+ if (existing) this.release(el);
948
+ const parsed = parse(attributes.source);
949
+ const config = resolveConfig(attributes, parsed);
950
+ const plan = compile(parsed, this.registry, config.timeline);
951
+ config.activation = this.resolveActivation(el, config, plan);
952
+ for (const warning of plan.warnings) this.reporter.warn(warning, el);
953
+ if (plan.fxNames.length === 0) {
954
+ el.setAttribute(ATTR.state, plan.unknown.length > 0 ? "pending" : "failed");
955
+ return;
956
+ }
957
+ this.install({ el, fingerprint, parsed, config, plan });
958
+ }
959
+ /**
960
+ * Apply a compiled plan and bind its activation.
961
+ *
962
+ * @complexity O(e) time in composed effects; O(e) space for retained cleanups.
963
+ * @overallScore 100
964
+ */
965
+ /**
966
+ * Choose the activation, letting a primitive's preference fill in only when the author named
967
+ * none, and warning when an authored activation is not one the effect supports.
968
+ *
969
+ * Declared capability metadata was previously never checked anywhere, which made
970
+ * `supportedActivations` documentation rather than a contract.
971
+ *
972
+ * @complexity O(a) time in supported activations; O(1) space.
973
+ * @overallScore 100
974
+ */
975
+ resolveActivation(el, config, plan) {
976
+ if (!config.activationAuthored) return plan.defaultActivation ?? config.activation;
977
+ const supported = plan.supportedActivations;
978
+ if (supported.length > 0 && !supported.includes(config.activation)) {
979
+ this.reporter.warn(
980
+ `activation "${config.activation}" is not supported by this effect (supports: ${supported.join(", ")})`,
981
+ el
982
+ );
983
+ }
984
+ return config.activation;
985
+ }
986
+ install(request) {
987
+ const { el, fingerprint, parsed, config, plan } = request;
988
+ const stylePlan = planStyles({
989
+ plan,
990
+ config,
991
+ capabilities: this.capabilities,
992
+ respectReducedMotion: this.respectReducedMotion
993
+ });
994
+ const ledger = createStyleLedger(el);
995
+ const attributes = createAttributeLedger(el);
996
+ const controller = new AbortController();
997
+ applyStylePlan({ el, plan: stylePlan, ledger, attributes });
998
+ const state = {
999
+ fingerprint,
1000
+ specs: parsed.specs,
1001
+ activation: config.activation,
1002
+ timeline: config.timeline,
1003
+ instances: [],
1004
+ ledger,
1005
+ attributes,
1006
+ controller,
1007
+ status: "ready"
1008
+ };
1009
+ this.states.set(el, state);
1010
+ this.liveElements.add(el);
1011
+ if (Object.keys(stylePlan.properties).some((property) => property.startsWith("animation-"))) {
1012
+ const animationNames = (plan.declarations["animation-name"] ?? "").split(",").map((name) => name.trim()).filter(Boolean);
1013
+ state.instances.push(createCssInstance(el, ledger, animationNames));
1014
+ }
1015
+ state.instances.push(
1016
+ ...this.jsEffectPreparer.prepare({ el, plan, signal: controller.signal, ledger })
1017
+ );
1018
+ this.openGate({ el, state, stylePlan, config, plan });
1019
+ }
1020
+ /**
1021
+ * Decide whether, and when, the effects on this element are allowed to start.
1022
+ *
1023
+ * The single place any effect begins. Routing both renderers through it is what makes
1024
+ * `on:enter`, `on:click`, `manual`, and `reducedMotion: 'disable'` mean the same thing for a
1025
+ * pinned section as for a fade — previously JS effects started during `prepare` and honoured
1026
+ * none of them.
1027
+ *
1028
+ * @complexity O(n) time in the number of instances; O(1) space.
1029
+ * @overallScore 100
1030
+ */
1031
+ openGate(request) {
1032
+ const { el, state, stylePlan, config, plan } = request;
1033
+ const reduce = this.respectReducedMotion && this.capabilities.reducedMotion;
1034
+ if (reduce && plan.reducedMotion === "disable") {
1035
+ state.status = "finished";
1036
+ state.attributes.set(ATTR.state, "finished");
1037
+ return;
1038
+ }
1039
+ if (stylePlan.gate !== "deferred") {
1040
+ this.activate(el);
1041
+ return;
1042
+ }
1043
+ state.controller.signal.addEventListener(
1044
+ "abort",
1045
+ this.binder.bind(el, stylePlan.activation, config.threshold, () => this.activate(el))
1046
+ );
1047
+ }
1048
+ /**
1049
+ * Start a deferred animation.
1050
+ *
1051
+ * A JS-rendered effect's real setup work is postponed until this call — `deferPrepare` in
1052
+ * `instances.ts` only wires up an inert instance during `prepare` — so a broken primitive (a bad
1053
+ * selector, a malformed param) first throws here, not while the plan was being built. `scan()`
1054
+ * reaches this synchronously for every `on:load` element, inside the very loop that processes
1055
+ * every other element on the page; an uncaught throw here previously unwound that loop and
1056
+ * silently orphaned every element after the broken one — the same blast radius the `__proto__`
1057
+ * scan-crash fix closed for a different door. Each instance is isolated so one effect's failure
1058
+ * can neither strand a sibling effect on the same element nor abort the rest of the scan.
1059
+ *
1060
+ * @complexity O(n) time in composed instances; O(1) space.
1061
+ * @overallScore 100
1062
+ */
1063
+ activate(el) {
1064
+ const state = this.states.get(el);
1065
+ if (!state || state.status === "running") return;
1066
+ state.status = "running";
1067
+ state.attributes.set(ATTR.state, "running");
1068
+ const started = state.instances.filter((instance) => this.startInstance(instance, el));
1069
+ if (started.length === 0 && state.instances.length > 0) {
1070
+ state.status = "failed";
1071
+ state.attributes.set(ATTR.state, "failed");
1072
+ return;
1073
+ }
1074
+ void Promise.all(started.map((instance) => instance.finished)).then(() => {
1075
+ if (this.states.get(el) !== state || state.status !== "running") return;
1076
+ state.status = "finished";
1077
+ state.attributes.set(ATTR.state, "finished");
1078
+ });
1079
+ }
1080
+ /**
1081
+ * Activate one instance, isolating a throw from its (possibly deferred) setup.
1082
+ *
1083
+ * @returns Whether the instance actually started — a failed instance is excluded from the
1084
+ * `finished` gate in `activate()`, since something that never started can never legitimately
1085
+ * finish (see `EffectInstance.finished`'s "resolves, never rejects" contract in `types.ts`).
1086
+ * @complexity O(1) time, O(1) space.
1087
+ * @overallScore 100
1088
+ */
1089
+ startInstance(instance, el) {
1090
+ try {
1091
+ instance.activate();
1092
+ return true;
1093
+ } catch (error) {
1094
+ this.reporter.warn(`effect failed to activate: ${String(error)}`, el);
1095
+ return false;
1096
+ }
1097
+ }
1098
+ /**
1099
+ * Programmatic entry point. Accepts a selector, an Element, a NodeList, or any iterable, so
1100
+ * `getElementById`, `getElementsByClassName`, and `querySelectorAll` all work directly.
1101
+ *
1102
+ * @complexity O(n) time in selected elements.
1103
+ * @overallScore 100
1104
+ */
1105
+ play(target, effect, options = {}) {
1106
+ return play({ animator: this, root: this.root, target, effect }, options);
1107
+ }
1108
+ /**
1109
+ * Remove the opt-in cloak so a stalled or failed initialisation can never leave a page hidden.
1110
+ *
1111
+ * @complexity O(1) time, O(1) space.
1112
+ * @overallScore 100
1113
+ */
1114
+ uncloak() {
1115
+ const doc = isElementNode(this.root) ? this.root.ownerDocument : this.root;
1116
+ doc?.documentElement?.removeAttribute(ATTR.cloak);
1117
+ }
1118
+ stateOf(el) {
1119
+ return this.states.get(el);
1120
+ }
1121
+ /**
1122
+ * Tear an element's effects down so the next `process()` reinstalls from scratch.
1123
+ *
1124
+ * Needed for replay: `process()` short-circuits when the configuration fingerprint is
1125
+ * unchanged, so playing the same effect twice was previously a no-op.
1126
+ *
1127
+ * @complexity O(c) time in retained instances; O(1) space.
1128
+ * @overallScore 100
1129
+ */
1130
+ reset(el) {
1131
+ this.release(el);
1132
+ }
1133
+ /**
1134
+ * Tear down one element's effects and clear its library-owned attributes.
1135
+ *
1136
+ * @complexity O(c) time in retained cleanups; O(1) extra space.
1137
+ * @overallScore 100
1138
+ */
1139
+ release(el) {
1140
+ const state = this.states.get(el);
1141
+ if (!state) return;
1142
+ state.controller.abort();
1143
+ for (const instance of state.instances) runQuietly(() => instance.destroy());
1144
+ this.states.delete(el);
1145
+ this.liveElements.delete(el);
1146
+ state.ledger.restore();
1147
+ state.attributes.restore();
1148
+ }
1149
+ /**
1150
+ * Tear down every tracked element inside a removed subtree.
1151
+ *
1152
+ * Scoped to `node`'s own descendants rather than re-scanning `liveElements` against the whole
1153
+ * page, so a removal event costs O(removed subtree), not O(every animated element alive
1154
+ * anywhere) — `dom-watcher.ts` can queue up to 100 removed roots per frame, and `liveElements`
1155
+ * only shrinks on release, so it stays large on a scroll-reveal-heavy page.
1156
+ *
1157
+ * Membership is checked against `liveElements` (the ground truth) rather than re-querying
1158
+ * `[${ATTR.source}]` the way `scan()` does: `dom-watcher.ts`'s `flush()` drains removed roots
1159
+ * before attribute-change roots, so if calling code strips `data-kui` and removes the element in
1160
+ * the same tick, a selector-based query would already miss it here and leak its teardown.
1161
+ *
1162
+ * `node` is typed `Element`, not `ParentNode`: `dom-watcher.ts`'s `onElementRemoved` — this
1163
+ * method's only caller — is itself typed `(el: Element) => void`, so there is no runtime case
1164
+ * where `node` is a `Document`/`DocumentFragment` to guard against.
1165
+ *
1166
+ * @complexity O(s) time in the removed subtree's element count; O(1) per candidate via the
1167
+ * `liveElements` Set lookup.
1168
+ * @overallScore 100
1169
+ */
1170
+ releaseTree(node) {
1171
+ if (this.liveElements.has(node)) this.release(node);
1172
+ for (const el of node.querySelectorAll("*")) {
1173
+ if (this.liveElements.has(el)) this.release(el);
1174
+ }
1175
+ }
1176
+ destroy() {
1177
+ this.domWatcher?.destroy();
1178
+ for (const el of [...this.liveElements]) this.release(el);
1179
+ this.binder.destroy();
1180
+ this.scheduler.destroy();
1181
+ this.started = false;
1182
+ }
1183
+ /**
1184
+ * Start watching for DOM insertions, removals, and attribute changes.
1185
+ *
1186
+ * Attribute changes recompile in place; insertions scan; removals tear down so listeners and
1187
+ * observers do not outlive their elements.
1188
+ *
1189
+ * @complexity O(1) time and space to build and start; the watcher's own callback runs O(n) time
1190
+ * in the nodes one mutation record carries.
1191
+ * @overallScore 100
1192
+ */
1193
+ watch() {
1194
+ this.domWatcher ??= createDomWatcher({
1195
+ root: this.root,
1196
+ onElementAdded: (el) => this.scan(el),
1197
+ onElementRemoved: (el) => this.releaseTree(el),
1198
+ onAttributeChanged: (el) => this.process(el)
1199
+ });
1200
+ this.domWatcher.watch();
1201
+ }
1202
+ };
1203
+ function createAnimator(options = {}) {
1204
+ return new Animator(options);
1205
+ }
1206
+ function resolveCollaborators(options) {
1207
+ const root = options.root ?? globalThis.document;
1208
+ const capabilities = options.capabilities ?? detect();
1209
+ const reporter = options.reporter ?? silentReporter();
1210
+ const scheduler = options.scheduler ?? createScrollScheduler();
1211
+ const rootResolver = options.rootResolver ?? defaultRootResolver(root);
1212
+ const respectReducedMotion = (options.reducedMotion ?? "respect") === "respect";
1213
+ return {
1214
+ registry: options.registry ?? new Registry(),
1215
+ capabilities,
1216
+ root,
1217
+ reporter,
1218
+ binder: options.binder ?? createActivationBinder(),
1219
+ scheduler,
1220
+ rootResolver,
1221
+ jsEffectPreparer: resolveJsEffectPreparer(options.jsEffectPreparer, {
1222
+ scheduler,
1223
+ rootResolver,
1224
+ capabilities,
1225
+ reporter,
1226
+ respectReducedMotion
1227
+ }),
1228
+ // Not defaulted here (unlike the other collaborators above): building the real watcher needs
1229
+ // `this.scan`/`this.process`/`this.releaseTree`, which don't exist yet inside this free
1230
+ // function. `Animator.watch()` builds it lazily instead, so nothing observes — and no
1231
+ // `MutationObserver` is ever constructed — unless `shouldObserve` is true and `start()` runs.
1232
+ domWatcher: options.domWatcher,
1233
+ respectReducedMotion,
1234
+ shouldObserve: options.observe ?? false
1235
+ };
1236
+ }
1237
+ function resolveJsEffectPreparer(provided, deps) {
1238
+ return provided ?? createJsEffectPreparer(deps);
1239
+ }
1240
+ function defaultRootResolver(root) {
1241
+ const doc = isElementNode(root) ? root.ownerDocument : root;
1242
+ const win = doc?.defaultView ?? globalThis;
1243
+ return createRootResolver({ win });
1244
+ }
1245
+ function runQuietly(cleanup) {
1246
+ try {
1247
+ cleanup();
1248
+ } catch {
1249
+ }
1250
+ }
1251
+
1252
+ export {
1253
+ ATTR,
1254
+ createActivationBinder,
1255
+ detect,
1256
+ resolveTargets,
1257
+ toAttributeValue,
1258
+ play,
1259
+ consoleReporter,
1260
+ silentReporter,
1261
+ collectingReporter,
1262
+ Animator,
1263
+ createAnimator
1264
+ };