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,2984 @@
1
+ // src/core/target.ts
2
+ function selectorBreadth(selector, doc) {
3
+ try {
4
+ if (doc.documentElement.matches(selector)) return "document-wide";
5
+ if (doc.body?.matches(selector)) return "document-wide";
6
+ return "ok";
7
+ } catch {
8
+ return "invalid";
9
+ }
10
+ }
11
+ function resolveTarget(selector, ctx, effect) {
12
+ if (!selector) return selector;
13
+ const breadth = selectorBreadth(selector, ctx.doc);
14
+ if (breadth === "invalid") {
15
+ ctx.warn(`${effect} target "${selector}" is not a valid selector and will be ignored`);
16
+ return "";
17
+ }
18
+ if (breadth === "document-wide") {
19
+ ctx.warn(`${effect} target "${selector}" matches the whole document and will be ignored`);
20
+ return "";
21
+ }
22
+ return selector;
23
+ }
24
+ var SCOPE_PARAM = {
25
+ type: "keyword",
26
+ default: "",
27
+ cssProperty: "--kui-scope",
28
+ keywords: ["self", "page"]
29
+ };
30
+ function scopeParam(params, fallback) {
31
+ const authored = params.text("scope");
32
+ if (authored === "page") return "page";
33
+ if (authored === "self") return "self";
34
+ return fallback;
35
+ }
36
+ function queryScoped(el, ctx, selector, scope) {
37
+ return [...(scope === "page" ? ctx.doc : el).querySelectorAll(selector)];
38
+ }
39
+
40
+ // src/core/event-sources.ts
41
+ function createEventSourceBindings() {
42
+ const bindings = /* @__PURE__ */ new Map();
43
+ function retain(source, type, run) {
44
+ let types = bindings.get(source);
45
+ if (!types) {
46
+ types = /* @__PURE__ */ new Map();
47
+ bindings.set(source, types);
48
+ }
49
+ let binding = types.get(type);
50
+ if (!binding) {
51
+ const runs = /* @__PURE__ */ new Set();
52
+ const listener = () => {
53
+ for (const callback of [...runs]) callback();
54
+ };
55
+ binding = { listener, runs };
56
+ types.set(type, binding);
57
+ source.addEventListener(type, listener, { passive: true });
58
+ }
59
+ binding.runs.add(run);
60
+ let retained = true;
61
+ return () => {
62
+ if (!retained) return;
63
+ retained = false;
64
+ binding.runs.delete(run);
65
+ if (binding.runs.size > 0) return;
66
+ source.removeEventListener(type, binding.listener);
67
+ types?.delete(type);
68
+ if (types?.size === 0) bindings.delete(source);
69
+ };
70
+ }
71
+ return {
72
+ bind({ sources, types, run }) {
73
+ const cleanups = [];
74
+ for (const source of sources) {
75
+ for (const type of types) cleanups.push(retain(source, type, run));
76
+ }
77
+ return () => {
78
+ for (const cleanup of cleanups) cleanup();
79
+ };
80
+ },
81
+ destroy() {
82
+ for (const [source, types] of bindings) {
83
+ for (const [type, binding] of types) source.removeEventListener(type, binding.listener);
84
+ }
85
+ bindings.clear();
86
+ }
87
+ };
88
+ }
89
+ function resolveEventSources({ el, from, reporter }) {
90
+ if (!from) return [el];
91
+ const doc = el.ownerDocument;
92
+ const breadth = selectorBreadth(from, doc);
93
+ if (breadth !== "ok") {
94
+ const reason = breadth === "invalid" ? "is not a valid selector" : "matches the whole document";
95
+ reporter?.warn(`activation source "${from}" ${reason} and will be ignored`, el);
96
+ return [];
97
+ }
98
+ const sources = [...doc.querySelectorAll(from)];
99
+ if (sources.length === 0) reporter?.warn(`activation source "${from}" matched nothing`, el);
100
+ return sources;
101
+ }
102
+
103
+ // src/core/threshold-reachability.ts
104
+ var RATIO_EPSILON = 1e-6;
105
+ function maxReachableRatio(entry) {
106
+ const root = entry.rootBounds;
107
+ const box = entry.boundingClientRect;
108
+ if (!root || box.width <= 0 || box.height <= 0) return void 0;
109
+ return Math.min(1, root.width / box.width) * Math.min(1, root.height / box.height);
110
+ }
111
+ function formatThreshold(ratio) {
112
+ return `${Math.round(ratio * 1e3) / 10}%`;
113
+ }
114
+ function checkThresholdReachability(binding, entry, ratio) {
115
+ const reporter = binding.reporter;
116
+ if (!reporter || ratio <= 0 || binding.thresholdWarned) return;
117
+ if (entry.isIntersecting) {
118
+ binding.sawIntersecting = true;
119
+ return;
120
+ }
121
+ if (!binding.sawIntersecting || binding.entered) return;
122
+ const max = maxReachableRatio(entry);
123
+ if (max === void 0 || max >= ratio - RATIO_EPSILON) return;
124
+ binding.thresholdWarned = true;
125
+ reporter.warn(
126
+ `threshold:${formatThreshold(ratio)} can never be met on this element \u2014 it is larger than the root it scrolls through, so its intersection ratio maxes out around ${formatThreshold(max)} and never reaches ${formatThreshold(ratio)}. An IntersectionObserver only reports ratio crossings, so the library cannot soften this from here \u2014 author a smaller threshold, or make the element (or its scrolling root) fit the other.`,
127
+ entry.target
128
+ );
129
+ }
130
+
131
+ // src/core/travel.ts
132
+ var TRAVEL_WINDOW_MS = 200;
133
+ function createTravelTracker() {
134
+ const positions = /* @__PURE__ */ new WeakMap();
135
+ let side;
136
+ let at = Number.NEGATIVE_INFINITY;
137
+ let holders = 0;
138
+ const onScroll = (event) => {
139
+ const target = event.target;
140
+ const now = scrollPositionOf(target);
141
+ if (!now) return;
142
+ const last = positions.get(target);
143
+ positions.set(target, now);
144
+ if (!last) return;
145
+ const dy = now.y - last.y;
146
+ const dx = now.x - last.x;
147
+ const delta = Math.abs(dy) >= Math.abs(dx) ? dy : dx;
148
+ if (delta === 0) return;
149
+ side = delta > 0 ? "after" : "before";
150
+ at = event.timeStamp;
151
+ };
152
+ const stop = () => {
153
+ holders = 0;
154
+ side = void 0;
155
+ at = Number.NEGATIVE_INFINITY;
156
+ if (typeof window !== "undefined") {
157
+ window.removeEventListener("scroll", onScroll, { capture: true });
158
+ }
159
+ };
160
+ return {
161
+ arrivedFrom: (time2) => time2 - at <= TRAVEL_WINDOW_MS ? side : void 0,
162
+ retain() {
163
+ if (holders++ > 0 || typeof window === "undefined") return;
164
+ window.addEventListener("scroll", onScroll, { capture: true, passive: true });
165
+ const start = scrollPositionOf(window.document);
166
+ if (start) positions.set(window.document, start);
167
+ },
168
+ release() {
169
+ if (holders === 0 || --holders > 0) return;
170
+ stop();
171
+ },
172
+ reset: stop
173
+ };
174
+ }
175
+ function scrollPositionOf(target) {
176
+ if (target === window || target === window.document) {
177
+ return { x: window.scrollX, y: window.scrollY };
178
+ }
179
+ if (target instanceof Element) return { x: target.scrollLeft, y: target.scrollTop };
180
+ return void 0;
181
+ }
182
+
183
+ // src/core/activation.ts
184
+ var PAIR_SEPARATOR = "/";
185
+ var NAMED_TRIGGERS = {
186
+ load: { kind: "immediate" },
187
+ manual: { kind: "manual" },
188
+ enter: { kind: "observed", when: "enter" },
189
+ leave: { kind: "observed", when: "leave" },
190
+ hover: { kind: "events", types: ["pointerenter", "focusin"] },
191
+ unhover: { kind: "events", types: ["pointerleave", "focusout"] },
192
+ focus: { kind: "events", types: ["focusin"] },
193
+ blur: { kind: "events", types: ["focusout"] },
194
+ click: { kind: "events", types: ["click"] }
195
+ };
196
+ var SUPPORT_PROXIES = {
197
+ load: ["load"],
198
+ manual: ["manual"],
199
+ enter: ["enter"],
200
+ leave: ["enter"],
201
+ hover: ["hover"],
202
+ unhover: ["hover"],
203
+ focus: ["focus"],
204
+ blur: ["focus"],
205
+ click: ["click"]
206
+ };
207
+ var EVENT_DRIVEN = ["hover", "focus", "click"];
208
+ var KNOWN_EVENTS = [
209
+ "click",
210
+ "dblclick",
211
+ "auxclick",
212
+ "contextmenu",
213
+ "pointerdown",
214
+ "pointerup",
215
+ "pointerenter",
216
+ "pointerleave",
217
+ "pointerover",
218
+ "pointerout",
219
+ "pointermove",
220
+ "pointercancel",
221
+ "gotpointercapture",
222
+ "lostpointercapture",
223
+ "mousedown",
224
+ "mouseup",
225
+ "mouseenter",
226
+ "mouseleave",
227
+ "mouseover",
228
+ "mouseout",
229
+ "mousemove",
230
+ "focusin",
231
+ "focusout",
232
+ "focus",
233
+ "blur",
234
+ "keydown",
235
+ "keyup",
236
+ "keypress",
237
+ "input",
238
+ "beforeinput",
239
+ "change",
240
+ "submit",
241
+ "reset",
242
+ "invalid",
243
+ "select",
244
+ "search",
245
+ "touchstart",
246
+ "touchend",
247
+ "touchmove",
248
+ "touchcancel",
249
+ "wheel",
250
+ "scroll",
251
+ "scrollend",
252
+ "dragstart",
253
+ "drag",
254
+ "dragend",
255
+ "dragenter",
256
+ "dragover",
257
+ "dragleave",
258
+ "drop",
259
+ "animationstart",
260
+ "animationend",
261
+ "animationiteration",
262
+ "animationcancel",
263
+ "transitionstart",
264
+ "transitionend",
265
+ "transitionrun",
266
+ "transitioncancel",
267
+ "play",
268
+ "playing",
269
+ "pause",
270
+ "ended",
271
+ "timeupdate",
272
+ "seeked",
273
+ "volumechange",
274
+ "ratechange",
275
+ "loadeddata",
276
+ "loadedmetadata",
277
+ "canplay",
278
+ "canplaythrough",
279
+ "waiting",
280
+ "stalled",
281
+ "toggle",
282
+ "beforetoggle",
283
+ "close",
284
+ "cancel",
285
+ "copy",
286
+ "cut",
287
+ "paste",
288
+ "load",
289
+ "error",
290
+ "abort",
291
+ "resize",
292
+ "fullscreenchange"
293
+ ];
294
+ var EVENT_NAME_RE = /^[A-Za-z][A-Za-z0-9._:-]*$/;
295
+ var SUGGESTION_DISTANCE = 2;
296
+ function isNamedActivation(name) {
297
+ return Object.hasOwn(NAMED_TRIGGERS, name);
298
+ }
299
+ function triggerFor(name) {
300
+ if (isNamedActivation(name)) return NAMED_TRIGGERS[name];
301
+ return { kind: "events", types: [name] };
302
+ }
303
+ function resolveActivationSpec(activation) {
304
+ const names = String(activation).split(PAIR_SEPARATOR);
305
+ const [start = "", end] = names;
306
+ return {
307
+ source: activation,
308
+ names,
309
+ start: triggerFor(start),
310
+ ...end === void 0 ? {} : { end: triggerFor(end) }
311
+ };
312
+ }
313
+ function validateActivation(value) {
314
+ const names = value.split(PAIR_SEPARATOR);
315
+ if (names.length > 2) {
316
+ return [
317
+ `activation "${value}" has more than one "${PAIR_SEPARATOR}" \u2014 expected "start${PAIR_SEPARATOR}end"`
318
+ ];
319
+ }
320
+ const malformed = names.filter((name) => !EVENT_NAME_RE.test(name));
321
+ if (malformed.length > 0) {
322
+ return [`activation "${value}" is not an event name or a "start${PAIR_SEPARATOR}end" pair`];
323
+ }
324
+ const end = names[1];
325
+ if (end !== void 0 && (end === "load" || end === "manual")) {
326
+ return [`activation "${value}" cannot end on "${end}" \u2014 an exit needs an event to fire on`];
327
+ }
328
+ return [];
329
+ }
330
+ function startKindOf(activation) {
331
+ return resolveActivationSpec(activation).start.kind;
332
+ }
333
+ function isOneShot(spec) {
334
+ return spec.end === void 0 && spec.start.kind === "observed";
335
+ }
336
+ function authorisingActivations(name) {
337
+ return isNamedActivation(name) ? SUPPORT_PROXIES[name] : EVENT_DRIVEN;
338
+ }
339
+ function isKnownEventType(type) {
340
+ if (/[-:.]/.test(type)) return true;
341
+ return KNOWN_EVENTS.includes(type);
342
+ }
343
+ function suggestActivation(name) {
344
+ let best;
345
+ let bestDistance = SUGGESTION_DISTANCE + 1;
346
+ for (const candidate of [...Object.keys(NAMED_TRIGGERS), ...KNOWN_EVENTS]) {
347
+ const distance2 = editDistance(name, candidate);
348
+ if (distance2 >= bestDistance) continue;
349
+ bestDistance = distance2;
350
+ best = candidate;
351
+ }
352
+ return bestDistance <= SUGGESTION_DISTANCE && bestDistance * 2 < name.length ? best : void 0;
353
+ }
354
+ function editDistance(a, b) {
355
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
356
+ for (let i = 1; i <= a.length; i++) {
357
+ const row = [i];
358
+ for (let j = 1; j <= b.length; j++) {
359
+ const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
360
+ row.push(Math.min(previous[j] + 1, row[j - 1] + 1, substitution));
361
+ }
362
+ previous = row;
363
+ }
364
+ return previous[b.length];
365
+ }
366
+ var NOOP = () => {
367
+ };
368
+ var RATIO_EPSILON2 = 1e-6;
369
+ function meetsThreshold(entry, ratio) {
370
+ if (!entry.isIntersecting) return false;
371
+ if (ratio <= 0) return true;
372
+ return entry.intersectionRatio >= ratio - RATIO_EPSILON2;
373
+ }
374
+ function deliverEntry(binding, entry, ratio, arrivedFrom) {
375
+ checkThresholdReachability(binding, entry, ratio);
376
+ if (binding.onCross) return deliverCrossing(binding, entry, ratio, arrivedFrom);
377
+ const inside = meetsThreshold(entry, ratio);
378
+ if (inside === binding.entered) return;
379
+ binding.entered = inside;
380
+ const side = inside ? binding.onEnter : binding.onLeave;
381
+ if (!side) return;
382
+ side();
383
+ if (binding.oneShot) binding.release();
384
+ }
385
+ function deliverCrossing(binding, entry, ratio, arrivedFrom) {
386
+ const side = sideOf(entry);
387
+ if (!meetsThreshold(entry, ratio)) return deliverLeaving(binding, side, arrivedFrom);
388
+ if (binding.entered) return;
389
+ const crossing = (arrivedFrom ?? binding.outside) === "before" ? "enter-back" : "enter";
390
+ binding.entered = true;
391
+ binding.outside = void 0;
392
+ binding.onCross?.(crossing);
393
+ }
394
+ function deliverLeaving(binding, side, arrivedFrom) {
395
+ if (side) binding.outside = side;
396
+ if (!binding.entered) return;
397
+ binding.entered = false;
398
+ const leavingTo = side ?? oppositeSide(arrivedFrom);
399
+ binding.onCross?.(leavingTo === "after" ? "leave-back" : "leave");
400
+ }
401
+ function oppositeSide(side) {
402
+ if (!side) return void 0;
403
+ return side === "after" ? "before" : "after";
404
+ }
405
+ function sideOf(entry) {
406
+ const root = entry.rootBounds;
407
+ const box = entry.boundingClientRect;
408
+ if (!root || !box) return void 0;
409
+ if (box.bottom <= root.top || box.right <= root.left) return "before";
410
+ if (box.top >= root.bottom || box.left >= root.right) return "after";
411
+ return void 0;
412
+ }
413
+ function createActivationBinder(options = {}) {
414
+ const observers = /* @__PURE__ */ new Map();
415
+ const callbacks = /* @__PURE__ */ new WeakMap();
416
+ const eventBindings = createEventSourceBindings();
417
+ const createObserver = options.createObserver ?? defaultObserverFactory();
418
+ const travel = createTravelTracker();
419
+ const reporter = options.reporter;
420
+ function observerFor(threshold) {
421
+ if (!createObserver) return void 0;
422
+ const ratio = toThresholdRatio(threshold);
423
+ const key = String(ratio);
424
+ const existing = observers.get(key);
425
+ if (existing) return { key, shared: existing };
426
+ const observer = createObserver(
427
+ (entries) => {
428
+ for (const entry of entries) {
429
+ const binding = callbacks.get(entry.target);
430
+ if (binding) deliverEntry(binding, entry, ratio, travel.arrivedFrom(entry.time));
431
+ }
432
+ },
433
+ { threshold: ratio }
434
+ );
435
+ const shared = { observer, count: 0 };
436
+ observers.set(key, shared);
437
+ return { key, shared };
438
+ }
439
+ function bindObserved(el, spec, request) {
440
+ const binding = observerFor(request.threshold);
441
+ if (!binding) {
442
+ if (spec.failOpen) request.activate();
443
+ return NOOP;
444
+ }
445
+ const { key, shared } = binding;
446
+ const tracksTravel = Boolean(spec.onCross);
447
+ if (tracksTravel) travel.retain();
448
+ let active = true;
449
+ const release = () => {
450
+ if (!active) return;
451
+ active = false;
452
+ if (tracksTravel) travel.release();
453
+ callbacks.delete(el);
454
+ shared.observer.unobserve(el);
455
+ shared.count--;
456
+ if (shared.count > 0) return;
457
+ shared.observer.disconnect();
458
+ observers.delete(key);
459
+ };
460
+ callbacks.set(el, {
461
+ onEnter: spec.onEnter,
462
+ onLeave: spec.onLeave,
463
+ ...spec.onCross ? { onCross: spec.onCross } : {},
464
+ oneShot: spec.oneShot,
465
+ release,
466
+ entered: false,
467
+ reporter,
468
+ sawIntersecting: false,
469
+ thresholdWarned: false
470
+ });
471
+ shared.count++;
472
+ shared.observer.observe(el);
473
+ return release;
474
+ }
475
+ return {
476
+ bind(el, activation, request) {
477
+ const spec = resolveActivationSpec(activation);
478
+ const sides = sidesOf(spec, request);
479
+ const cleanups = [];
480
+ let eventSources;
481
+ const observed = observedSides(sides);
482
+ if (observed) {
483
+ const failOpen = spec.start.kind === "observed";
484
+ const crossing = request.cross;
485
+ cleanups.push(
486
+ bindObserved(
487
+ el,
488
+ {
489
+ ...observed,
490
+ ...crossing ? { onCross: crossing } : {},
491
+ oneShot: crossing ? false : isOneShot(spec),
492
+ failOpen
493
+ },
494
+ request
495
+ )
496
+ );
497
+ }
498
+ for (const side of sides) {
499
+ const { trigger } = side;
500
+ if (trigger.kind === "events") {
501
+ eventSources ??= resolveEventSources({ el, from: request.from, reporter });
502
+ cleanups.push(eventBindings.bind({ sources: eventSources, types: trigger.types, run: side.run }));
503
+ } else if (trigger.kind === "immediate") side.run();
504
+ }
505
+ return cleanups.length === 0 ? NOOP : () => {
506
+ for (const cleanup of cleanups) cleanup();
507
+ };
508
+ },
509
+ destroy() {
510
+ for (const { observer } of observers.values()) observer.disconnect();
511
+ observers.clear();
512
+ eventBindings.destroy();
513
+ travel.reset();
514
+ }
515
+ };
516
+ }
517
+ function sidesOf(spec, request) {
518
+ const sides = [{ trigger: spec.start, run: () => request.activate() }];
519
+ const deactivate = request.deactivate;
520
+ if (spec.end && deactivate) sides.push({ trigger: spec.end, run: () => deactivate() });
521
+ return sides;
522
+ }
523
+ function observedSides(sides) {
524
+ const observed = {};
525
+ let found = false;
526
+ for (const side of sides) {
527
+ if (side.trigger.kind !== "observed") continue;
528
+ found = true;
529
+ if (side.trigger.when === "enter") observed.onEnter = side.run;
530
+ else observed.onLeave = side.run;
531
+ }
532
+ return found ? observed : void 0;
533
+ }
534
+ function toThresholdRatio(raw) {
535
+ const value = Number.parseFloat(raw);
536
+ if (Number.isNaN(value)) return 0;
537
+ const ratio = raw.includes("%") ? value / 100 : value;
538
+ return Math.min(1, Math.max(0, ratio));
539
+ }
540
+ function defaultObserverFactory() {
541
+ if (typeof IntersectionObserver === "undefined") return void 0;
542
+ return (callback, init) => new IntersectionObserver(callback, init);
543
+ }
544
+ function warnAboutActivation(request) {
545
+ warnUnsupported(request);
546
+ warnUnknownEvents(request);
547
+ }
548
+ function warnUnsupported({ el, spec, supported, reporter }) {
549
+ if (supported.length === 0) return;
550
+ for (const name of spec.names) {
551
+ if (authorisingActivations(name).some((named) => supported.includes(named))) continue;
552
+ reporter.warn(
553
+ `activation "${name}" is not supported by this effect (supports: ${supported.join(", ")})`,
554
+ el
555
+ );
556
+ }
557
+ }
558
+ function warnUnknownEvents({ el, spec, reporter }) {
559
+ if (!("onclick" in el)) return;
560
+ for (const name of spec.names) {
561
+ if (isNamedActivation(name) || isKnownEventType(name) || `on${name}` in el) continue;
562
+ const suggestion = suggestActivation(name);
563
+ const hint = suggestion ? `; did you mean "${suggestion}"?` : "";
564
+ const problem = `no DOM event named "${name}" \u2014 data-kui-on binds it anyway, so nothing will start it`;
565
+ reporter.warn(`${problem}${hint}`, el);
566
+ }
567
+ }
568
+
569
+ // src/core/attrs.ts
570
+ var ATTR = {
571
+ /** Authored. The rich grammar. */
572
+ source: "data-kui",
573
+ /**
574
+ * Authored. Names the composition in this element's {@link source} so other elements can reuse
575
+ * it by name. An element carrying it is a definition — data, never animated. See `core/bundles.ts`.
576
+ */
577
+ define: "data-kui-define",
578
+ /** Library-owned and unstable: normalized effect names, for CSS hooks and debugging. */
579
+ normalized: "data-kui-fx",
580
+ state: "data-kui-state",
581
+ on: "data-kui-on",
582
+ timeline: "data-kui-timeline",
583
+ threshold: "data-kui-threshold",
584
+ stagger: "data-kui-stagger",
585
+ cloak: "data-kui-cloak",
586
+ /** Reduced-motion policy, stamped from the primitive so the CSS layer can act on it. */
587
+ rm: "data-kui-rm",
588
+ /** Library-owned: comma list of `target:` selectors on this host that matched nothing (D). */
589
+ unmatched: "data-kui-unmatched",
590
+ /** Library-owned: a structural role the library assigned to a child (`front`,`back`,`control`,`bar`). */
591
+ part: "data-kui-part"
592
+ };
593
+
594
+ // src/core/types.ts
595
+ var CHANNEL = {
596
+ opacity: "opacity",
597
+ translate: "translate",
598
+ scale: "scale",
599
+ rotate: "rotate",
600
+ filter: "filter",
601
+ clip: "clip",
602
+ background: "background",
603
+ color: "color",
604
+ stroke: "stroke",
605
+ text: "text",
606
+ /**
607
+ * Skew is the one transform CSS never gave an independent property to — there is no `skew:`
608
+ * beside `translate:`/`rotate:`/`scale:`, so it can only be written through the `transform`
609
+ * shorthand. That makes it its own channel: anything writing `transform` clobbers the whole
610
+ * shorthand, so every primitive that does is on this channel, whatever the transform is for.
611
+ * `scroll-skew` is one member; `flip-face` (`effects/three-d`) and `flip-3d`
612
+ * (`effects/catalog/core`, the `flip-in-*`/`flip-out-*` family) are the other two — both need
613
+ * the `perspective()` transform *function* for an element to have depth on itself, which
614
+ * likewise only exists inside `transform`.
615
+ */
616
+ skew: "skew"
617
+ };
618
+ function inertInstance(destroy = () => {
619
+ }) {
620
+ return {
621
+ activate() {
622
+ },
623
+ cancel() {
624
+ },
625
+ finish() {
626
+ },
627
+ finished: Promise.resolve(),
628
+ destroy
629
+ };
630
+ }
631
+ var SEGMENT_HOIST_KEYS = [
632
+ "activation",
633
+ "actions",
634
+ "timeline",
635
+ "threshold",
636
+ "cascade",
637
+ "spread",
638
+ "order",
639
+ "cols",
640
+ "along"
641
+ ];
642
+
643
+ // src/core/registry.ts
644
+ var Registry = class {
645
+ primitives = /* @__PURE__ */ new Map();
646
+ presets = /* @__PURE__ */ new Map();
647
+ /** Sorted-name key → preset that renders that combination as one tested keyframe. */
648
+ combos = /* @__PURE__ */ new Map();
649
+ registerPrimitive(primitive) {
650
+ if (this.primitives.has(primitive.id)) {
651
+ throw new Error(`kuinetic: primitive "${primitive.id}" is already registered`);
652
+ }
653
+ this.primitives.set(primitive.id, namespaceTiming(primitive));
654
+ return this;
655
+ }
656
+ registerPreset(preset) {
657
+ if (this.presets.has(preset.name)) {
658
+ throw new Error(`kuinetic: effect "${preset.name}" is already registered`);
659
+ }
660
+ if (!this.primitives.has(preset.primitive)) {
661
+ throw new Error(
662
+ `kuinetic: effect "${preset.name}" references unknown primitive "${preset.primitive}"`
663
+ );
664
+ }
665
+ for (const segment of preset.transitions ?? []) {
666
+ if (segment.property === "all" || segment.property === "none") {
667
+ throw new Error(
668
+ `kuinetic: effect "${preset.name}" declares a transition on "${segment.property}", which would swallow every other preset's segment in the merged transition list \u2014 name the real property instead`
669
+ );
670
+ }
671
+ }
672
+ this.presets.set(preset.name, preset);
673
+ return this;
674
+ }
675
+ registerPresets(presets) {
676
+ for (const preset of presets) this.registerPreset(preset);
677
+ return this;
678
+ }
679
+ registerPrimitives(primitives) {
680
+ for (const primitive of primitives) this.registerPrimitive(primitive);
681
+ return this;
682
+ }
683
+ /**
684
+ * Declare that a set of effect names has a purpose-built single-keyframe implementation.
685
+ * Checked before channel conflict analysis, so `fade-up` + `blur-in` can resolve to the
686
+ * tested `fade-blur-up` rather than being rejected for both writing `opacity`.
687
+ */
688
+ registerCombo(names, presetName) {
689
+ this.combos.set(comboKey(names), presetName);
690
+ return this;
691
+ }
692
+ resolve(name) {
693
+ const preset = this.presets.get(name);
694
+ if (!preset) return void 0;
695
+ const primitive = this.primitives.get(preset.primitive);
696
+ return { preset, primitive };
697
+ }
698
+ findCombo(names) {
699
+ const comboName = this.combos.get(comboKey(names));
700
+ return comboName ? this.resolve(comboName) : void 0;
701
+ }
702
+ has(name) {
703
+ return this.presets.has(name);
704
+ }
705
+ /** All registered effect names, for docs generation and dev-mode "did you mean" hints. */
706
+ names() {
707
+ return [...this.presets.keys()].sort((a, b) => a.localeCompare(b));
708
+ }
709
+ getPrimitive(id) {
710
+ return this.primitives.get(id);
711
+ }
712
+ };
713
+ var TIMING_PARAMS = ["duration", "delay", "ease"];
714
+ function timingProperty(primitiveId, name) {
715
+ return `--kui-${primitiveId}-${name}`;
716
+ }
717
+ function namespaceTiming(primitive) {
718
+ const parameters = { ...primitive.parameters };
719
+ for (const name of TIMING_PARAMS) {
720
+ const spec = parameters[name];
721
+ if (spec) parameters[name] = { ...spec, cssProperty: timingProperty(primitive.id, name) };
722
+ }
723
+ return { ...primitive, parameters };
724
+ }
725
+ function comboKey(names) {
726
+ return [...names].sort((a, b) => a.localeCompare(b)).join("+");
727
+ }
728
+ function suggest(name, candidates) {
729
+ let best;
730
+ let bestScore = Infinity;
731
+ for (const candidate of candidates) {
732
+ const score = distance(name, candidate);
733
+ if (score < bestScore) {
734
+ bestScore = score;
735
+ best = candidate;
736
+ }
737
+ }
738
+ return bestScore <= Math.max(2, Math.floor(name.length / 3)) ? best : void 0;
739
+ }
740
+ function distance(a, b) {
741
+ const rows = a.length + 1;
742
+ const cols = b.length + 1;
743
+ let prev = Array.from({ length: cols }, (_, i) => i);
744
+ for (let i = 1; i < rows; i++) {
745
+ const curr = [i, ...Array(cols - 1).fill(0)];
746
+ for (let j = 1; j < cols; j++) {
747
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
748
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
749
+ }
750
+ prev = curr;
751
+ }
752
+ return prev[cols - 1];
753
+ }
754
+
755
+ // src/core/breakpoints.ts
756
+ var BREAKPOINTS = {
757
+ sm: "40rem",
758
+ md: "48rem",
759
+ lg: "64rem",
760
+ xl: "80rem",
761
+ "2xl": "96rem"
762
+ };
763
+ var BREAKPOINT_NAMES = Object.keys(BREAKPOINTS);
764
+ var GATE_AXES = [
765
+ ["above", "below"],
766
+ ["wide", "narrow"]
767
+ ];
768
+ function axisOf(direction) {
769
+ return GATE_AXES.find((axis) => axis.includes(direction));
770
+ }
771
+ function isBreakpoint(value) {
772
+ return Object.hasOwn(BREAKPOINTS, value);
773
+ }
774
+ function breakpointRank(name) {
775
+ return BREAKPOINT_NAMES.indexOf(name);
776
+ }
777
+ function gateProperty(direction, breakpoint) {
778
+ return `--kui-${direction}-${breakpoint}`;
779
+ }
780
+ function breakpointQuery(breakpoint) {
781
+ return `(min-width: ${BREAKPOINTS[breakpoint]})`;
782
+ }
783
+ function gatedAnimationName(keyframes, gate) {
784
+ if (!gate) return keyframes;
785
+ let expression = keyframes;
786
+ if (gate.narrow) expression = `var(${gateProperty("narrow", gate.narrow)}, ${expression})`;
787
+ if (gate.wide) expression = `var(${gateProperty("wide", gate.wide)}, ${expression})`;
788
+ if (gate.below) expression = `var(${gateProperty("below", gate.below)}, ${expression})`;
789
+ if (gate.above) expression = `var(${gateProperty("above", gate.above)}, ${expression})`;
790
+ return expression;
791
+ }
792
+ function gateMatches(gate, win) {
793
+ if (!gate) return true;
794
+ if (typeof win?.matchMedia !== "function") return true;
795
+ const atLeast = (breakpoint) => win.matchMedia(breakpointQuery(breakpoint)).matches;
796
+ if (gate.above && !atLeast(gate.above)) return false;
797
+ if (gate.below && atLeast(gate.below)) return false;
798
+ return true;
799
+ }
800
+ function gatesOverlap(a, b) {
801
+ if (!a || !b) return true;
802
+ const span = (gate, [upper, lower]) => [
803
+ gate[upper] ? breakpointRank(gate[upper]) : -1,
804
+ gate[lower] ? breakpointRank(gate[lower]) : BREAKPOINT_NAMES.length
805
+ ];
806
+ const overlapsOn = (axis) => {
807
+ const [aStart, aEnd] = span(a, axis);
808
+ const [bStart, bEnd] = span(b, axis);
809
+ return aStart < bEnd && bStart < aEnd;
810
+ };
811
+ return GATE_AXES.every(overlapsOn);
812
+ }
813
+ function breakpointsIn(gates) {
814
+ const named = /* @__PURE__ */ new Set();
815
+ for (const gate of gates) {
816
+ if (gate.above) named.add(gate.above);
817
+ if (gate.below) named.add(gate.below);
818
+ }
819
+ return [...named];
820
+ }
821
+ function createGateWatcher(win, onChange) {
822
+ const watched = /* @__PURE__ */ new Map();
823
+ const bound = /* @__PURE__ */ new Map();
824
+ function bind(breakpoint) {
825
+ if (typeof win?.matchMedia !== "function" || bound.has(breakpoint)) return;
826
+ const query = win.matchMedia(breakpointQuery(breakpoint));
827
+ if (typeof query.addEventListener !== "function") return;
828
+ const handler = () => {
829
+ for (const [el, names] of [...watched]) {
830
+ if (names.includes(breakpoint)) onChange(el);
831
+ }
832
+ };
833
+ query.addEventListener("change", handler);
834
+ bound.set(breakpoint, () => query.removeEventListener("change", handler));
835
+ }
836
+ return {
837
+ watch(el, breakpoints) {
838
+ if (breakpoints.length === 0) {
839
+ watched.delete(el);
840
+ return;
841
+ }
842
+ watched.set(el, [...breakpoints]);
843
+ for (const breakpoint of breakpoints) bind(breakpoint);
844
+ },
845
+ unwatch(el) {
846
+ watched.delete(el);
847
+ },
848
+ destroy() {
849
+ for (const unbind of bound.values()) unbind();
850
+ bound.clear();
851
+ watched.clear();
852
+ }
853
+ };
854
+ }
855
+
856
+ // src/core/spring.ts
857
+ var DEFAULT_SPRING = {
858
+ stiffness: 180,
859
+ damping: 24,
860
+ mass: 1,
861
+ restVelocity: 0.05,
862
+ restDisplacement: 0.05
863
+ };
864
+ var SUBSTEP = 1 / 240;
865
+ var MAX_STEP = 0.25;
866
+ function stepSpring(state, target, config, dt) {
867
+ let { value, velocity } = state;
868
+ let remaining = Math.min(Math.max(dt, 0), MAX_STEP);
869
+ while (remaining > 0) {
870
+ const step = Math.min(SUBSTEP, remaining);
871
+ const force = -config.stiffness * (value - target) - config.damping * velocity;
872
+ velocity += force / config.mass * step;
873
+ value += velocity * step;
874
+ remaining -= step;
875
+ }
876
+ return { value, velocity };
877
+ }
878
+ function isSettled(state, target, config) {
879
+ return Math.abs(state.velocity) < config.restVelocity && Math.abs(state.value - target) < config.restDisplacement;
880
+ }
881
+ var MAX_SETTLE_MS = 1e4;
882
+ function createSpringRunner(config, onChange, deps) {
883
+ const safeConfig = validConfig(config) ? config : DEFAULT_SPRING;
884
+ if (safeConfig !== config) deps.warn?.("invalid spring configuration; using defaults");
885
+ let state = { value: 0, velocity: 0 };
886
+ let target = 0;
887
+ let handle = null;
888
+ let lastTime = 0;
889
+ let startedAt = 0;
890
+ function frame(time2) {
891
+ handle = null;
892
+ const dt = (time2 - lastTime) / 1e3;
893
+ lastTime = time2;
894
+ state = stepSpring(state, target, safeConfig, dt);
895
+ if (!finiteState(state) || !Number.isFinite(target)) {
896
+ abortRun("spring produced non-finite state");
897
+ return;
898
+ }
899
+ if (time2 - startedAt >= MAX_SETTLE_MS) {
900
+ abortRun(`spring exceeded ${MAX_SETTLE_MS}ms settle budget`);
901
+ return;
902
+ }
903
+ if (isSettled(state, target, safeConfig)) {
904
+ state = { value: target, velocity: 0 };
905
+ onChange(state.value, true);
906
+ return;
907
+ }
908
+ onChange(state.value, false);
909
+ schedule();
910
+ }
911
+ function schedule() {
912
+ if (handle !== null) return;
913
+ handle = deps.requestFrame(frame);
914
+ }
915
+ function start() {
916
+ startedAt = deps.now();
917
+ if (handle === null) lastTime = startedAt;
918
+ schedule();
919
+ }
920
+ function abortRun(message) {
921
+ state = { value: Number.isFinite(target) ? target : 0, velocity: 0 };
922
+ deps.warn?.(message);
923
+ onChange(state.value, true);
924
+ }
925
+ return {
926
+ to(next, velocity) {
927
+ target = next;
928
+ if (velocity !== void 0) state = { ...state, velocity };
929
+ start();
930
+ },
931
+ set(value, velocity = 0) {
932
+ state = { value, velocity };
933
+ },
934
+ current: () => ({ ...state }),
935
+ stop() {
936
+ if (handle !== null) deps.cancelFrame(handle);
937
+ handle = null;
938
+ }
939
+ };
940
+ }
941
+ function validConfig(config) {
942
+ return Object.values(config).every(Number.isFinite) && config.stiffness > 0 && config.damping > 0 && config.mass > 0 && config.restVelocity > 0 && config.restDisplacement > 0;
943
+ }
944
+ function finiteState(state) {
945
+ return Number.isFinite(state.value) && Number.isFinite(state.velocity);
946
+ }
947
+ function defaultSpringDeps() {
948
+ const raf = globalThis.requestAnimationFrame;
949
+ if (typeof raf !== "function") {
950
+ return {
951
+ requestFrame: (callback) => globalThis.setTimeout(() => callback(Date.now()), 16),
952
+ cancelFrame: (handle) => globalThis.clearTimeout(handle),
953
+ now: () => Date.now()
954
+ };
955
+ }
956
+ return {
957
+ requestFrame: (callback) => raf(callback),
958
+ cancelFrame: (handle) => globalThis.cancelAnimationFrame(handle),
959
+ now: () => performance.now()
960
+ };
961
+ }
962
+
963
+ // src/core/easing.ts
964
+ var NATIVE_EASINGS = /* @__PURE__ */ new Set([
965
+ "linear",
966
+ "ease",
967
+ "ease-in",
968
+ "ease-out",
969
+ "ease-in-out",
970
+ "step-start",
971
+ "step-end"
972
+ ]);
973
+ var SPRING_PREFIX = "spring(";
974
+ var DEFAULT_RATIO = 2 / 3;
975
+ var DEFAULT_STIFFNESS = 180;
976
+ var DEFAULT_MASS = 1;
977
+ var DEFAULT_DAMPING = 2 * DEFAULT_RATIO * Math.sqrt(DEFAULT_STIFFNESS * DEFAULT_MASS);
978
+ var MIN_RATIO = 0.15;
979
+ var MAX_RATIO = 4;
980
+ var MAX_BOUNCE = 1 - MIN_RATIO;
981
+ var PHYSICS_KEYS = ["stiffness", "damping", "mass"];
982
+ var SPRING_KEYS = ["bounce", ...PHYSICS_KEYS];
983
+ var SIM_STEP = 1 / 240;
984
+ var MAX_SIM_STEPS = 4e4;
985
+ var REST_DISPLACEMENT = 1e-3;
986
+ var REST_VELOCITY = 1e-3;
987
+ var MIN_SAMPLES = 16;
988
+ var MAX_SAMPLES = 64;
989
+ var SAMPLES_PER_PERIOD = 10;
990
+ var PRECISION = 1e3;
991
+ function isSpringToken(token) {
992
+ return token.startsWith(SPRING_PREFIX) && token.endsWith(")");
993
+ }
994
+ function cssEasingValue(easing) {
995
+ if (NATIVE_EASINGS.has(easing)) return easing;
996
+ if (isSpringToken(easing)) return springLinearCurve(springDampingRatio(easing));
997
+ if (easing.includes("(")) return easing;
998
+ return `var(--kui-ease-${easing}, ease-out)`;
999
+ }
1000
+ var WAAPI_FALLBACK = "ease-out";
1001
+ function waapiEasingValue(easing, el, warn) {
1002
+ if (!easing) return void 0;
1003
+ if (NATIVE_EASINGS.has(easing)) return easing;
1004
+ if (isSpringToken(easing)) return springLinearCurve(springDampingRatio(easing));
1005
+ if (easing.includes("(")) return easing;
1006
+ const defined = customEasing(easing, el);
1007
+ if (defined) return defined;
1008
+ warn(`easing "${easing}" has no --kui-ease-${easing} definition \u2014 using ${WAAPI_FALLBACK}`);
1009
+ return WAAPI_FALLBACK;
1010
+ }
1011
+ function customEasing(name, el) {
1012
+ const view = el.ownerDocument.defaultView;
1013
+ if (!view) return "";
1014
+ return view.getComputedStyle(el).getPropertyValue(`--kui-ease-${name}`).trim();
1015
+ }
1016
+ function springTokenProblems(token) {
1017
+ if (!isSpringToken(token)) return [];
1018
+ const { values, problems } = readArguments(token);
1019
+ resolveRatio(values, problems);
1020
+ return problems;
1021
+ }
1022
+ function springDampingRatio(token) {
1023
+ const { values, problems } = readArguments(token);
1024
+ return resolveRatio(values, problems);
1025
+ }
1026
+ function readArguments(token) {
1027
+ const values = /* @__PURE__ */ new Map();
1028
+ const problems = [];
1029
+ const body = token.slice(SPRING_PREFIX.length, -1).trim();
1030
+ if (body) {
1031
+ for (const part of body.split(/\s+/)) readArgument(part, values, problems);
1032
+ }
1033
+ return { values, problems };
1034
+ }
1035
+ function readArgument(part, values, problems) {
1036
+ const colon = part.indexOf(":");
1037
+ const key = colon < 0 ? part : part.slice(0, colon);
1038
+ const raw = colon < 0 ? "" : part.slice(colon + 1);
1039
+ if (!SPRING_KEYS.includes(key)) {
1040
+ problems.push(`unknown spring parameter "${part}" \u2014 expected ${SPRING_KEYS.join(", ")}`);
1041
+ return;
1042
+ }
1043
+ const numeric = Number(raw);
1044
+ if (raw === "" || !Number.isFinite(numeric) || numeric < 0) {
1045
+ problems.push(`spring "${key}" expects a non-negative number \u2014 got "${raw}"`);
1046
+ return;
1047
+ }
1048
+ if (values.has(key)) problems.push(`duplicate spring parameter "${key}"`);
1049
+ values.set(key, numeric);
1050
+ }
1051
+ function resolveRatio(values, problems) {
1052
+ const bounce = values.get("bounce");
1053
+ const hasPhysics = PHYSICS_KEYS.some((key) => values.has(key));
1054
+ if (bounce !== void 0) {
1055
+ if (hasPhysics) {
1056
+ problems.push(
1057
+ `spring "bounce" and "${PHYSICS_KEYS.join('"/"')}" describe the same curve two ways \u2014 "bounce" wins`
1058
+ );
1059
+ }
1060
+ return 1 - clampBounce(bounce, problems);
1061
+ }
1062
+ if (!hasPhysics) return DEFAULT_RATIO;
1063
+ const stiffness = values.get("stiffness") ?? DEFAULT_STIFFNESS;
1064
+ const mass = values.get("mass") ?? DEFAULT_MASS;
1065
+ const damping = values.get("damping") ?? DEFAULT_DAMPING;
1066
+ return clampRatio(damping / (2 * Math.sqrt(stiffness * mass)), problems);
1067
+ }
1068
+ function clampBounce(bounce, problems) {
1069
+ if (bounce <= MAX_BOUNCE) return bounce;
1070
+ problems.push(
1071
+ `spring "bounce:${bounce}" is past ${MAX_BOUNCE} \u2014 a spring that bouncy never settles inside one animation, so it is capped at ${MAX_BOUNCE}`
1072
+ );
1073
+ return MAX_BOUNCE;
1074
+ }
1075
+ function clampRatio(ratio, problems) {
1076
+ if (!Number.isFinite(ratio)) {
1077
+ problems.push("spring constants do not describe a spring \u2014 using the default curve");
1078
+ return DEFAULT_RATIO;
1079
+ }
1080
+ if (ratio >= MIN_RATIO && ratio <= MAX_RATIO) return ratio;
1081
+ const clamped = Math.min(Math.max(ratio, MIN_RATIO), MAX_RATIO);
1082
+ problems.push(
1083
+ `spring damping ratio ${round(ratio)} is outside ${MIN_RATIO}\u2013${MAX_RATIO} \u2014 clamped to ${round(clamped)} so the curve settles`
1084
+ );
1085
+ return clamped;
1086
+ }
1087
+ var curves = /* @__PURE__ */ new Map();
1088
+ function springLinearCurve(dampingRatio) {
1089
+ const key = Math.round(dampingRatio * PRECISION);
1090
+ const cached = curves.get(key);
1091
+ if (cached !== void 0) return cached;
1092
+ const curve = buildCurve(key / PRECISION);
1093
+ curves.set(key, curve);
1094
+ return curve;
1095
+ }
1096
+ function buildCurve(ratio) {
1097
+ const trajectory = simulate(ratio);
1098
+ const count = sampleCount(ratio, (trajectory.length - 1) * SIM_STEP);
1099
+ const stops = [];
1100
+ for (let index = 0; index <= count; index++) {
1101
+ stops.push(String(round(sampleAt(trajectory, index / count))));
1102
+ }
1103
+ stops[0] = "0";
1104
+ stops[count] = "1";
1105
+ return `linear(${stops.join(", ")})`;
1106
+ }
1107
+ function simulate(ratio) {
1108
+ const config = {
1109
+ stiffness: 1,
1110
+ damping: 2 * ratio,
1111
+ mass: 1,
1112
+ restVelocity: REST_VELOCITY,
1113
+ restDisplacement: REST_DISPLACEMENT
1114
+ };
1115
+ const values = [0];
1116
+ let state = { value: 0, velocity: 0 };
1117
+ for (let step = 0; step < MAX_SIM_STEPS; step++) {
1118
+ state = stepSpring(state, 1, config, SIM_STEP);
1119
+ values.push(state.value);
1120
+ if (isSettled(state, 1, config)) break;
1121
+ }
1122
+ return values;
1123
+ }
1124
+ function sampleCount(ratio, settleTime) {
1125
+ if (ratio >= 1) return MIN_SAMPLES;
1126
+ const period = 2 * Math.PI / Math.sqrt(1 - ratio * ratio);
1127
+ const stops = MIN_SAMPLES + Math.round(SAMPLES_PER_PERIOD * settleTime / period);
1128
+ return Math.min(stops, MAX_SAMPLES);
1129
+ }
1130
+ function sampleAt(values, fraction) {
1131
+ const position = fraction * (values.length - 1);
1132
+ const low = Math.floor(position);
1133
+ const high = Math.min(low + 1, values.length - 1);
1134
+ const start = values[low];
1135
+ return start + (values[high] - start) * (position - low);
1136
+ }
1137
+ function round(value) {
1138
+ return Math.round(value * PRECISION) / PRECISION;
1139
+ }
1140
+
1141
+ // src/core/repeat.ts
1142
+ var INFINITE = "infinite";
1143
+ var COUNT_RE = /^(?:\d+(?:\.\d+)?|\.\d+)$/;
1144
+ var PROGRESS_DRIVEN = /* @__PURE__ */ new Set(["view", "scroll", "pin"]);
1145
+ var PLAYBACK_KEYS = /* @__PURE__ */ new Set(["repeat", "yoyo"]);
1146
+ function isPlaybackKey(key) {
1147
+ return PLAYBACK_KEYS.has(key);
1148
+ }
1149
+ function applyPlayback(spec, key, value, context) {
1150
+ if (spec[key] !== void 0) {
1151
+ context.warn(`duplicate parameter "${key}" in "${context.segment}"`);
1152
+ }
1153
+ if (key === "yoyo") applyYoyo(spec, value, context);
1154
+ else applyRepeat(spec, value, context);
1155
+ }
1156
+ function applyRepeat(spec, value, context) {
1157
+ if (value === INFINITE) {
1158
+ spec.repeat = INFINITE;
1159
+ return;
1160
+ }
1161
+ if (!COUNT_RE.test(value)) {
1162
+ context.warn(countRefusal(value, context.segment));
1163
+ return;
1164
+ }
1165
+ if (Number(value) === 0) {
1166
+ context.warn(
1167
+ `"repeat:0" in "${context.segment}" means the effect never plays \u2014 with animation-fill-mode: both it jumps straight to its end state. Write "repeat:1" to play it once, or drop the key.`
1168
+ );
1169
+ }
1170
+ spec.repeat = value;
1171
+ }
1172
+ function applyYoyo(spec, value, context) {
1173
+ if (value !== "true" && value !== "false") {
1174
+ context.warn(
1175
+ `"yoyo:${value}" in "${context.segment}" is not a boolean \u2014 expected yoyo:true or yoyo:false`
1176
+ );
1177
+ return;
1178
+ }
1179
+ spec.yoyo = value === "true";
1180
+ }
1181
+ function countRefusal(value, segment) {
1182
+ if (value.startsWith("-")) {
1183
+ return `"repeat:${value}" in "${segment}" is negative \u2014 a play count cannot be. Write "repeat:infinite" to loop forever, or a non-negative number such as repeat:3.`;
1184
+ }
1185
+ return `"repeat:${value}" in "${segment}" is not a play count \u2014 expected a non-negative number such as repeat:3, or repeat:infinite`;
1186
+ }
1187
+ function resolvePlayback(input) {
1188
+ const warnings = [];
1189
+ if (input.repeat === void 0 && input.yoyo === void 0) return { warnings };
1190
+ if (input.renderer !== "css-keyframes") return { warnings: [rendererRefusal(input)] };
1191
+ if (input.repeat === INFINITE && PROGRESS_DRIVEN.has(input.timeline)) {
1192
+ warnings.push(infiniteTimelineRefusal(input.name, input.timeline));
1193
+ return { yoyo: input.yoyo, warnings };
1194
+ }
1195
+ const hidden = endsHidden(input);
1196
+ if (hidden) warnings.push(hidden);
1197
+ return { repeat: input.repeat, yoyo: input.yoyo, warnings };
1198
+ }
1199
+ function endsHidden(input) {
1200
+ if (!input.cloak || input.yoyo !== true) return null;
1201
+ const count = repeatCount(input.repeat);
1202
+ if (count === void 0 || count === 0 || count % 2 !== 0) return null;
1203
+ return `"${input.name}" with yoyo:true and an even repeat:${input.repeat} ends on a reversed iteration, and this effect's from-state is one the visitor is not meant to see \u2014 the element finishes hidden. Use an odd count (repeat:${count + 1}) to end at the rest state.`;
1204
+ }
1205
+ function rendererRefusal(input) {
1206
+ const authored = [
1207
+ input.repeat === void 0 ? "" : `repeat:${input.repeat}`,
1208
+ input.yoyo === void 0 ? "" : `yoyo:${input.yoyo}`
1209
+ ].filter(Boolean).join(" and ");
1210
+ return `"${input.name}" is rendered in JavaScript and compiles no animation-iteration-count, so ${authored} does nothing \u2014 it plays once. Repeat is a CSS-keyframes capability; a JS-rendered effect that loops does it through a parameter of its own (typewriter's "loop:"), if it has one.`;
1211
+ }
1212
+ function infiniteTimelineRefusal(name, timeline) {
1213
+ const because = timeline === "pin" ? `a pin scrubs a single playthrough with a negative animation-delay, so no iteration past the first is ever reachable` : `a "${timeline}" timeline is a finite range driven by scroll position rather than a clock, and an infinite iteration count collapses the animation's active duration to zero there \u2014 the element renders frozen at its end state`;
1214
+ return `"${name}" has repeat:infinite on a "${timeline}" timeline: ${because}. Dropping the repeat, so it plays once across the range \u2014 put an infinite loop on a "time" timeline instead.`;
1215
+ }
1216
+ function repeatCount(repeat) {
1217
+ if (repeat === void 0 || repeat === INFINITE) return void 0;
1218
+ return Number(repeat);
1219
+ }
1220
+ function isInfiniteRepeat(repeat) {
1221
+ return repeat === INFINITE;
1222
+ }
1223
+ function directionValue(yoyo) {
1224
+ return yoyo ? "alternate" : "normal";
1225
+ }
1226
+ function playbackExpression(duration, repeat) {
1227
+ const count = repeatCount(repeat);
1228
+ if (count === void 0 || count === 1) return duration;
1229
+ return `${duration} * ${count}`;
1230
+ }
1231
+
1232
+ // src/core/toggle-actions.ts
1233
+ var CROSSINGS = ["enter", "leave", "enter-back", "leave-back"];
1234
+ var VERBS = /* @__PURE__ */ new Set([
1235
+ "play",
1236
+ "pause",
1237
+ "resume",
1238
+ "reverse",
1239
+ "reset",
1240
+ "restart",
1241
+ "complete",
1242
+ "none"
1243
+ ]);
1244
+ var PLAYHEAD_VERBS = /* @__PURE__ */ new Set([
1245
+ "pause",
1246
+ "resume",
1247
+ "reset",
1248
+ "restart",
1249
+ "complete"
1250
+ ]);
1251
+ var SEPARATOR = "/";
1252
+ function parseToggleActions(value, warnings = []) {
1253
+ const parts = value.split(SEPARATOR);
1254
+ if (parts.length > CROSSINGS.length) {
1255
+ warnings.push(
1256
+ `"actions:${value}" has ${String(parts.length)} verbs \u2014 there are four crossings (${CROSSINGS.join(SEPARATOR)}), so the extras are ignored`
1257
+ );
1258
+ }
1259
+ const actions = {
1260
+ "enter": "none",
1261
+ "leave": "none",
1262
+ "enter-back": "none",
1263
+ "leave-back": "none"
1264
+ };
1265
+ for (const [index, crossing] of CROSSINGS.entries()) {
1266
+ const raw = parts[index]?.trim();
1267
+ if (raw === void 0 || raw === "") continue;
1268
+ actions[crossing] = toVerb(raw, crossing, warnings);
1269
+ }
1270
+ return actions;
1271
+ }
1272
+ function toVerb(raw, crossing, warnings) {
1273
+ if (VERBS.has(raw)) return raw;
1274
+ warnings.push(
1275
+ `unrecognised action "${raw}" for the "${crossing}" crossing \u2014 expected ${[...VERBS].join(", ")}`
1276
+ );
1277
+ return "none";
1278
+ }
1279
+ function validateToggleActions(value) {
1280
+ const warnings = [];
1281
+ parseToggleActions(value, warnings);
1282
+ return warnings;
1283
+ }
1284
+ function usesPlayhead(actions) {
1285
+ for (const crossing of CROSSINGS) {
1286
+ if (PLAYHEAD_VERBS.has(actions[crossing])) return actions[crossing];
1287
+ }
1288
+ return void 0;
1289
+ }
1290
+ function warnAboutToggleActions(request) {
1291
+ if (!request.observed) {
1292
+ return [
1293
+ `"actions:" describes the four crossings of a scroll trigger, but this element activates on "${request.activation}" \u2014 nothing here is ever reached. Use on:enter or on:enter/leave`
1294
+ ];
1295
+ }
1296
+ const verb = usesPlayhead(request.actions);
1297
+ if (verb === void 0) return [];
1298
+ const warnings = [];
1299
+ if (request.progressDriven) {
1300
+ warnings.push(
1301
+ `"actions:" asks to ${verb} an element whose progress is driven by scroll position rather than by a clock \u2014 its playhead belongs to the scroller and would be overwritten on the next frame`
1302
+ );
1303
+ }
1304
+ const js = request.jsEffectNames;
1305
+ if (js.length > 0) {
1306
+ const one = js.length === 1;
1307
+ warnings.push(
1308
+ `"actions:" asks to ${verb}, which needs a playhead, and "${js.join(" ")}" ${one ? "is" : "are"} rendered in JavaScript and expose${one ? "s" : ""} none \u2014 that crossing does nothing for ${one ? "it" : "them"}`
1309
+ );
1310
+ }
1311
+ return warnings;
1312
+ }
1313
+ function applyToggleVerb(verb, target) {
1314
+ if (verb === "play") target.activate();
1315
+ else if (verb === "reverse") target.reverse();
1316
+ else if (PLAYHEAD_VERBS.has(verb)) applyPlayheadVerb(verb, target);
1317
+ }
1318
+ function applyPlayheadVerb(verb, target) {
1319
+ if (!target.started) {
1320
+ if (startsIt(verb)) target.activate();
1321
+ return;
1322
+ }
1323
+ if (verb === "restart" || verb === "complete") target.activate();
1324
+ for (const control2 of target.controls) applyToPlayhead(verb, control2);
1325
+ }
1326
+ function startsIt(verb) {
1327
+ return verb === "resume" || verb === "restart" || verb === "complete";
1328
+ }
1329
+ function applyToPlayhead(verb, control2) {
1330
+ if (verb === "pause") control2.pause();
1331
+ else if (verb === "resume") control2.resume();
1332
+ else if (verb === "complete") control2.seek(1);
1333
+ else {
1334
+ control2.seek(0);
1335
+ if (verb === "reset") control2.pause();
1336
+ else control2.resume();
1337
+ }
1338
+ }
1339
+
1340
+ // src/core/parse.ts
1341
+ var TIME_RE = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:ms|s)$/;
1342
+ var EASING_FUNCTIONS = ["cubic-bezier(", "steps(", "linear(", "spring("];
1343
+ var EASING_KEYWORDS = /* @__PURE__ */ new Set([
1344
+ "linear",
1345
+ "ease",
1346
+ "ease-in",
1347
+ "ease-out",
1348
+ "ease-in-out",
1349
+ "step-start",
1350
+ "step-end",
1351
+ "expo-in",
1352
+ "expo-out",
1353
+ "expo-in-out",
1354
+ "back-in",
1355
+ "back-out",
1356
+ "back-in-out",
1357
+ "quart-out",
1358
+ "circ-out",
1359
+ "spring",
1360
+ "bounce"
1361
+ ]);
1362
+ function splitTopLevel(input, delimiter, warnings = []) {
1363
+ const parts = [];
1364
+ const scanner = { depth: 0, quote: null, escaped: false };
1365
+ let buffer = "";
1366
+ for (const char of input) {
1367
+ if (isSeparator(char, delimiter, scanner)) {
1368
+ if (buffer.trim()) parts.push(buffer.trim());
1369
+ buffer = "";
1370
+ continue;
1371
+ }
1372
+ buffer += char;
1373
+ }
1374
+ if (buffer.trim()) parts.push(buffer.trim());
1375
+ if (scanner.quote) warnings.push(`unterminated ${scanner.quote} quote in "${input}"`);
1376
+ else if (scanner.depth > 0) warnings.push(`unclosed "(" in "${input}"`);
1377
+ return parts;
1378
+ }
1379
+ function isSeparator(char, delimiter, scanner) {
1380
+ if (scanner.quote) return advanceQuote(char, scanner);
1381
+ if (char === '"' || char === "'") {
1382
+ scanner.quote = char;
1383
+ return false;
1384
+ }
1385
+ if (char === "(") scanner.depth++;
1386
+ else if (char === ")") scanner.depth = Math.max(0, scanner.depth - 1);
1387
+ if (scanner.depth !== 0) return false;
1388
+ return delimiter === " " ? /\s/.test(char) : char === delimiter;
1389
+ }
1390
+ function advanceQuote(char, scanner) {
1391
+ if (scanner.escaped) scanner.escaped = false;
1392
+ else if (char === "\\") scanner.escaped = true;
1393
+ else if (char === scanner.quote) scanner.quote = null;
1394
+ return false;
1395
+ }
1396
+ function splitPair(token) {
1397
+ let depth = 0;
1398
+ for (let i = 0; i < token.length; i++) {
1399
+ const char = token[i];
1400
+ if (char === "(") depth++;
1401
+ else if (char === ")") depth = Math.max(0, depth - 1);
1402
+ else if (char === ":" && depth === 0) {
1403
+ const key = token.slice(0, i).trim();
1404
+ const value = unquote(token.slice(i + 1).trim());
1405
+ return key && value ? [key, value] : null;
1406
+ }
1407
+ }
1408
+ return null;
1409
+ }
1410
+ function unquote(value) {
1411
+ const first = value[0];
1412
+ if ((first === '"' || first === "'") && value.endsWith(first) && value.length > 1) {
1413
+ return value.slice(1, -1).replaceAll(`\\${first}`, first);
1414
+ }
1415
+ return value;
1416
+ }
1417
+ function parseActivationAttribute(input) {
1418
+ const warnings = [];
1419
+ const tokens = splitTopLevel(input ?? "", " ", warnings);
1420
+ const [activation, ...options] = tokens;
1421
+ if (!activation) return { warnings };
1422
+ warnings.push(...validateActivation(activation));
1423
+ let from;
1424
+ for (const option of options) {
1425
+ const pair = splitPair(option);
1426
+ if (!pair || pair[0] !== "from") {
1427
+ warnings.push(`unrecognised activation option "${option}"`);
1428
+ continue;
1429
+ }
1430
+ if (from !== void 0) {
1431
+ warnings.push(`duplicate activation option "from" in "${input}"`);
1432
+ continue;
1433
+ }
1434
+ from = pair[1];
1435
+ }
1436
+ if (warnings.length > 0) return { warnings };
1437
+ return { activation, ...from === void 0 ? {} : { from }, warnings };
1438
+ }
1439
+ function classify(token) {
1440
+ const pair = splitPair(token);
1441
+ if (pair) return { kind: "pair", key: pair[0], value: pair[1] };
1442
+ if (TIME_RE.test(token)) return { kind: "time", value: token };
1443
+ if (isEasing(token)) return { kind: "easing", value: token };
1444
+ return { kind: "unknown", value: token };
1445
+ }
1446
+ function isEasing(token) {
1447
+ if (EASING_KEYWORDS.has(token)) return true;
1448
+ return EASING_FUNCTIONS.some((fn) => token.startsWith(fn) && token.endsWith(")"));
1449
+ }
1450
+ function parse(input) {
1451
+ const result = { specs: [], warnings: [] };
1452
+ for (const segment of splitTopLevel(input ?? "", ",", result.warnings)) {
1453
+ const spec = parseSegment(segment, result);
1454
+ if (spec) result.specs.push(spec);
1455
+ }
1456
+ return result;
1457
+ }
1458
+ function parseSegment(segment, result) {
1459
+ const tokens = splitTopLevel(segment, " ", result.warnings);
1460
+ const name = tokens.shift();
1461
+ if (splitPair(name)) {
1462
+ if (applyGroupOnlySegment(name, tokens, result)) return null;
1463
+ result.warnings.push(`effect name expected, got "${name}"`);
1464
+ return null;
1465
+ }
1466
+ const spec = { name, params: {} };
1467
+ let timeCount = 0;
1468
+ const sink = hasTargetToken(tokens) ? { specs: [], warnings: [] } : null;
1469
+ for (const raw of tokens) {
1470
+ const token = classify(raw);
1471
+ if (token.kind === "time") timeCount = applyTime(spec, token.value, timeCount, result.warnings);
1472
+ else applyToken(token, spec, { segment, result, sink });
1473
+ }
1474
+ attachSegmentHoists(spec, sink, result);
1475
+ return spec;
1476
+ }
1477
+ function attachSegmentHoists(spec, sink, result) {
1478
+ if (!sink) return;
1479
+ result.warnings.push(...sink.warnings);
1480
+ const hoists = collectHoists(sink);
1481
+ if (hoists) spec.hoists = hoists;
1482
+ }
1483
+ function hasTargetToken(tokens) {
1484
+ return tokens.some((t) => splitPair(t)?.[0] === "target");
1485
+ }
1486
+ function collectHoists(sink) {
1487
+ const hoists = {};
1488
+ let any = false;
1489
+ for (const key of SEGMENT_HOIST_KEYS) {
1490
+ const value = sink[key];
1491
+ if (value === void 0) continue;
1492
+ hoists[key] = value;
1493
+ any = true;
1494
+ }
1495
+ return any ? hoists : void 0;
1496
+ }
1497
+ var SEGMENT_SCOPABLE_HOISTS = /* @__PURE__ */ new Set([
1498
+ "on",
1499
+ "actions",
1500
+ "timeline",
1501
+ "threshold",
1502
+ "cascade",
1503
+ "spread",
1504
+ "order",
1505
+ "cols",
1506
+ "along"
1507
+ ]);
1508
+ function applyGroupOnlySegment(first, rest, result) {
1509
+ const pairs = [];
1510
+ for (const token of [first, ...rest]) {
1511
+ const pair = splitPair(token);
1512
+ if (!pair || !GROUP_ONLY_HOISTS.has(pair[0]) || !Object.hasOwn(HOISTS, pair[0])) return false;
1513
+ pairs.push(pair);
1514
+ }
1515
+ for (const [key, value] of pairs) HOISTS[key](result, value);
1516
+ return true;
1517
+ }
1518
+ var GROUP_ONLY_HOISTS = /* @__PURE__ */ new Set([
1519
+ "cascade",
1520
+ "spread",
1521
+ "order",
1522
+ "cols",
1523
+ "along"
1524
+ ]);
1525
+ function applyTime(spec, value, seen, warnings) {
1526
+ if (seen === 0) spec.duration = value;
1527
+ else if (seen === 1) spec.delay = value;
1528
+ else warnings.push(`third time value "${value}" ignored (expected duration then delay)`);
1529
+ return seen + 1;
1530
+ }
1531
+ function applyToken(token, spec, context) {
1532
+ const { segment, result, sink } = context;
1533
+ if (token.kind === "easing") {
1534
+ applyEasing(token.value, spec, segment, result);
1535
+ return;
1536
+ }
1537
+ if (token.kind === "unknown") {
1538
+ result.warnings.push(
1539
+ `unrecognised token "${token.value}" in "${segment}" \u2014 expected [duration] [delay] [easing] or key:value`
1540
+ );
1541
+ return;
1542
+ }
1543
+ if (applyLifted(token.key, token.value, spec, context)) return;
1544
+ if (Object.hasOwn(HOISTS, token.key)) {
1545
+ const dest = sink && SEGMENT_SCOPABLE_HOISTS.has(token.key) ? sink : result;
1546
+ HOISTS[token.key](dest, token.value);
1547
+ return;
1548
+ }
1549
+ if (token.key in spec.params) {
1550
+ result.warnings.push(`duplicate parameter "${token.key}" in "${segment}"`);
1551
+ }
1552
+ spec.params[token.key] = token.value;
1553
+ }
1554
+ function applyEasing(value, spec, segment, result) {
1555
+ if (spec.easing) result.warnings.push(`duplicate easing "${value}" in "${segment}"`);
1556
+ for (const problem of springTokenProblems(value)) {
1557
+ result.warnings.push(`${problem} in "${segment}"`);
1558
+ }
1559
+ spec.easing = value;
1560
+ }
1561
+ function applyLifted(key, value, spec, context) {
1562
+ const { segment, result } = context;
1563
+ if (key === "at") {
1564
+ if (spec.at !== void 0) result.warnings.push(`duplicate parameter "at" in "${segment}"`);
1565
+ spec.at = value;
1566
+ return true;
1567
+ }
1568
+ if (isGateDirection(key)) {
1569
+ applyGate(spec, key, value, context);
1570
+ return true;
1571
+ }
1572
+ if (isPlaybackKey(key)) {
1573
+ applyPlayback(spec, key, value, {
1574
+ segment,
1575
+ warn: (message) => result.warnings.push(message)
1576
+ });
1577
+ return true;
1578
+ }
1579
+ return false;
1580
+ }
1581
+ var GATE_DIRECTIONS = /* @__PURE__ */ new Set(["above", "below", "wide", "narrow"]);
1582
+ function isGateDirection(key) {
1583
+ return GATE_DIRECTIONS.has(key);
1584
+ }
1585
+ function applyGate(spec, direction, value, context) {
1586
+ const { segment, result } = context;
1587
+ if (!isBreakpoint(value)) {
1588
+ result.warnings.push(
1589
+ `unknown breakpoint "${value}" in "${segment}" \u2014 expected one of ${BREAKPOINT_NAMES.join(", ")}`
1590
+ );
1591
+ return;
1592
+ }
1593
+ const gate = spec.gate ??= {};
1594
+ if (gate[direction] !== void 0) {
1595
+ result.warnings.push(`duplicate parameter "${direction}" in "${segment}"`);
1596
+ }
1597
+ gate[direction] = value;
1598
+ warnOnEmptyBand(gate, axisOf(direction), segment, result);
1599
+ }
1600
+ function warnOnEmptyBand(gate, axis, segment, result) {
1601
+ const [upperName, lowerName] = axis;
1602
+ const upperValue = gate[upperName];
1603
+ const lowerValue = gate[lowerName];
1604
+ if (!upperValue || !lowerValue || breakpointRank(upperValue) < breakpointRank(lowerValue)) return;
1605
+ result.warnings.push(
1606
+ `"${upperName}:${upperValue} ${lowerName}:${lowerValue}" in "${segment}" can never match \u2014 "${upperName}" must name a smaller breakpoint than "${lowerName}"`
1607
+ );
1608
+ }
1609
+ var RM_POLICIES = /* @__PURE__ */ new Set(["shorten", "crossfade", "disable"]);
1610
+ var HOISTS = {
1611
+ /**
1612
+ * The activation list is open: any event type `addEventListener` accepts starts an animation,
1613
+ * and `start/end` pairs it with an exit. So this no longer checks the value against a closed set
1614
+ * of six names — `on:input` and `on:cart:updated` are both legitimate and unguessable from here.
1615
+ *
1616
+ * What it still rejects is text that cannot be an event type at all, because that is where the
1617
+ * open list would otherwise turn a typo into silence rather than a warning. The complementary
1618
+ * check — "this document has never heard of that event" — needs an element and lives in
1619
+ * `animator.ts`.
1620
+ */
1621
+ on(result, value) {
1622
+ const problems = validateActivation(value);
1623
+ if (problems.length > 0) {
1624
+ result.warnings.push(...problems);
1625
+ return;
1626
+ }
1627
+ assignOnce(result, "activation", value, "activations");
1628
+ },
1629
+ timeline(result, value) {
1630
+ assignOnce(result, "timeline", value, "timelines");
1631
+ },
1632
+ /**
1633
+ * What to do at each of a scroll trigger's four crossings —
1634
+ * `actions:play/pause/resume/reset`, in the order enter, leave, enter-back, leave-back.
1635
+ *
1636
+ * Element-scoped for `on:`'s reason and then some: it *refines* `on:`, and an element has one
1637
+ * activation to refine. Validated here against the closed verb set, exactly as `rm:` is and for
1638
+ * the same reason — there is nothing a later stage could know about the word `pasue` that this
1639
+ * one does not. The checks that need the element's compiled plan (is this activation even
1640
+ * observed? can these effects be paused at all?) live in `toggle-actions.ts` and run from
1641
+ * `animator.ts`.
1642
+ */
1643
+ actions(result, value) {
1644
+ const problems = validateToggleActions(value);
1645
+ if (problems.length > 0) {
1646
+ result.warnings.push(...problems);
1647
+ return;
1648
+ }
1649
+ assignOnce(result, "actions", value, "crossing actions");
1650
+ },
1651
+ threshold(result, value) {
1652
+ assignOnce(result, "threshold", value, "thresholds");
1653
+ },
1654
+ /**
1655
+ * Deliberately unvalidated here. `data-kui-stagger` has always written its step straight into
1656
+ * `--kui-stagger`, which is what makes `var(--speed)` and `calc(90ms * 2)` work today, and the
1657
+ * two spellings have to accept the same values or "move it into `data-kui`" would silently be a
1658
+ * narrowing. `stagger.ts` owns the one screen both spellings get.
1659
+ */
1660
+ cascade(result, value) {
1661
+ warnStepMode(result, "spread", "cascade", value);
1662
+ assignOnce(result, "cascade", value, "stagger steps");
1663
+ },
1664
+ /**
1665
+ * The total-time spelling of the same setting — GSAP's `stagger.amount` beside `cascade`'s
1666
+ * `stagger.each`. `cascade:50ms` on a 200-item list takes ten seconds; `spread:600ms` takes six
1667
+ * hundred milliseconds however many items there are, because `stagger.ts` divides the budget by
1668
+ * the group's largest rank.
1669
+ *
1670
+ * Unvalidated here for exactly `cascade`'s reason: the value is divided inside a `calc()` and
1671
+ * written to `--kui-stagger`, so `var(--speed)` and `calc(1s - 200ms)` have to survive.
1672
+ */
1673
+ spread(result, value) {
1674
+ warnStepMode(result, "cascade", "spread", value);
1675
+ assignOnce(result, "spread", value, "stagger budgets");
1676
+ },
1677
+ /**
1678
+ * Also unvalidated here, for a different reason: the legal set depends on the *group size*
1679
+ * (`order:7` is in range for eight children and clamped for three), which only `stagger.ts`
1680
+ * knows. Validating the keyword half here and the index half there would split one diagnostic
1681
+ * across two modules.
1682
+ */
1683
+ order(result, value) {
1684
+ assignOnce(result, "order", value, "stagger orders");
1685
+ },
1686
+ /**
1687
+ * The group's column count, which is what makes `order:center` mean the middle *cell* rather than
1688
+ * the middle *index*. Unvalidated here for `order`'s reason: `cols:auto` is only resolvable
1689
+ * against a laid-out group, and the count that is legal depends on the group size, so the whole
1690
+ * diagnostic lives in `stagger.ts` where both are known.
1691
+ */
1692
+ cols(result, value) {
1693
+ assignOnce(result, "cols", value, "stagger column counts");
1694
+ },
1695
+ /**
1696
+ * The axis a grid stagger is restricted to.
1697
+ *
1698
+ * Spelled `along:` rather than GSAP's `axis:` for the same reason `order:` is not `from:`: `axis`
1699
+ * is a parameter on four primitives (`parallax`, `slat-assemble`, and both draggables) and a
1700
+ * hoisted key never reaches `spec.params`, so lifting the word element-wide would make
1701
+ * `data-kui="parallax-y axis:x"` unwritable. `data-kui-stagger` accepts both spellings.
1702
+ */
1703
+ along(result, value) {
1704
+ assignOnce(result, "along", value, "stagger axes");
1705
+ },
1706
+ /**
1707
+ * The one hoist that is not a move.
1708
+ *
1709
+ * `data-kui-rm` is *output*, not input: `style-plan.ts` stamps it from `plan.reducedMotion`,
1710
+ * which `compile.ts` folds out of the composed primitives' own declared policies, and ~40
1711
+ * selectors in `base.css` key on it. No author has ever written it, so there was nothing to
1712
+ * hoist — what this adds is the ability to *choose* the policy, which the library had no
1713
+ * spelling for at all. The stamped attribute keeps its exact meaning ("the policy in force
1714
+ * here"), so every one of those selectors is untouched.
1715
+ *
1716
+ * Validated against the closed set here rather than in `compile.ts`, so a typo (`rm:disabled`)
1717
+ * is named at the point the author's text is read, next to every other grammar diagnostic.
1718
+ */
1719
+ rm(result, value) {
1720
+ if (!RM_POLICIES.has(value)) {
1721
+ result.warnings.push(
1722
+ `unrecognised "rm:${value}" \u2014 expected ${[...RM_POLICIES].join(", ")}`
1723
+ );
1724
+ return;
1725
+ }
1726
+ assignOnce(result, "rm", value, "reduced-motion policies");
1727
+ },
1728
+ /**
1729
+ * The name of a global function to call when this element's effects finish.
1730
+ *
1731
+ * Element-scoped for the same reason `on:` is: one element has one lifecycle, so a second,
1732
+ * different `func:` across the comma list would be describing a completion this element only
1733
+ * reaches once. `assignOnce` names that conflict rather than letting token order pick a winner.
1734
+ *
1735
+ * Unvalidated here, and there is nothing useful to validate. `window['my-fn'] = …` is legal
1736
+ * JavaScript, so an identifier-shaped regex would reject working names; and whether the name
1737
+ * resolves at all depends on script order at *runtime*, which this module cannot see. `callback.ts`
1738
+ * owns the lookup, the `typeof` check, and the one diagnostic — see the security note at the top
1739
+ * of that file before putting `func:` anywhere a CMS field can reach.
1740
+ */
1741
+ func(result, value) {
1742
+ assignOnce(result, "func", value, "callbacks");
1743
+ }
1744
+ };
1745
+ function warnStepMode(result, other, key, value) {
1746
+ const written = result[other];
1747
+ if (written === void 0) return;
1748
+ result.warnings.push(
1749
+ `"${other}:${written}" and "${key}:${value}" are two ways to set one stagger step \u2014 the total budget ("spread:") wins`
1750
+ );
1751
+ }
1752
+ function assignOnce(result, key, value, label) {
1753
+ const current = result[key];
1754
+ if (current === void 0) {
1755
+ result[key] = value;
1756
+ return;
1757
+ }
1758
+ if (current !== value) {
1759
+ result.warnings.push(`conflicting ${label} "${String(current)}" and "${String(value)}"`);
1760
+ }
1761
+ }
1762
+
1763
+ // src/core/play.ts
1764
+ function resolveTargets(target, root) {
1765
+ if (typeof target === "string") return [...root.querySelectorAll(target)];
1766
+ if (target instanceof Element) return [target];
1767
+ return [...target];
1768
+ }
1769
+ function time(value) {
1770
+ if (value === void 0) return void 0;
1771
+ return typeof value === "number" ? `${value}ms` : value;
1772
+ }
1773
+ var STRUCTURAL_RE = /[\s,()"']/;
1774
+ function quoteIfNeeded(value) {
1775
+ if (!STRUCTURAL_RE.test(value)) return value;
1776
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1777
+ return `"${escaped}"`;
1778
+ }
1779
+ function hasTopLevelColon(value) {
1780
+ let depth = 0;
1781
+ for (const char of value) {
1782
+ if (char === "(") depth++;
1783
+ else if (char === ")") depth = Math.max(0, depth - 1);
1784
+ else if (char === ":" && depth === 0) return true;
1785
+ }
1786
+ return false;
1787
+ }
1788
+ function assertBareToken(label, value) {
1789
+ const warnings = [];
1790
+ const bySpace = splitTopLevel(value, " ", warnings);
1791
+ const byComma = splitTopLevel(value, ",", warnings);
1792
+ const isSingleToken = bySpace.length === 1 && bySpace[0] === value;
1793
+ const safe = warnings.length === 0 && isSingleToken && byComma.length === 1 && !hasTopLevelColon(value);
1794
+ if (!safe) {
1795
+ throw new Error(
1796
+ `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`
1797
+ );
1798
+ }
1799
+ }
1800
+ function toAttributeValue(effect, options = {}) {
1801
+ assertBareToken("effect", effect);
1802
+ const { duration, delay, ease, ...rest } = options;
1803
+ delete rest.stagger;
1804
+ const parts = [effect];
1805
+ const resolvedDelay = time(delay);
1806
+ const resolvedDuration = time(duration);
1807
+ if (resolvedDuration) {
1808
+ assertBareToken("duration", resolvedDuration);
1809
+ parts.push(resolvedDuration);
1810
+ }
1811
+ if (resolvedDelay) {
1812
+ assertBareToken("delay", resolvedDelay);
1813
+ parts.push(resolvedDuration ? resolvedDelay : `delay:${resolvedDelay}`);
1814
+ }
1815
+ if (ease) {
1816
+ assertBareToken("easing", ease);
1817
+ parts.push(ease);
1818
+ }
1819
+ for (const [key, value] of Object.entries(rest)) {
1820
+ if (value === void 0) continue;
1821
+ assertBareToken("parameter name", key);
1822
+ parts.push(`${key}:${quoteIfNeeded(String(value))}`);
1823
+ }
1824
+ return parts.join(" ");
1825
+ }
1826
+ function play(request, options = {}) {
1827
+ const { animator, root, target, effect } = request;
1828
+ const elements = resolveTargets(target, root);
1829
+ const stagger = time(options.stagger);
1830
+ const source = toAttributeValue(effect, options);
1831
+ for (const [index, el] of elements.entries()) {
1832
+ if (stagger) {
1833
+ ;
1834
+ el.style.setProperty("--kui-stagger", stagger);
1835
+ el.style.setProperty("--kui-i", String(index));
1836
+ }
1837
+ animator.reset(el);
1838
+ if (!el.hasAttribute(ATTR.on)) el.setAttribute(ATTR.on, "manual");
1839
+ el.setAttribute(ATTR.source, source);
1840
+ animator.process(el);
1841
+ animator.activate(el);
1842
+ }
1843
+ const instancesOf = (el) => [el, ...animator.derivedOf(el)].flatMap((each) => animator.stateOf(each)?.instances ?? []);
1844
+ const finished = Promise.all(
1845
+ elements.flatMap((el) => instancesOf(el).map((instance) => instance.finished))
1846
+ ).then(() => void 0);
1847
+ return {
1848
+ elements,
1849
+ finished,
1850
+ cancel() {
1851
+ for (const el of elements) animator.cancel(el);
1852
+ },
1853
+ finish() {
1854
+ for (const el of elements) for (const instance of instancesOf(el)) instance.finish();
1855
+ }
1856
+ };
1857
+ }
1858
+
1859
+ // src/core/control.ts
1860
+ var MEASURABLE_SPAN_MS = 0;
1861
+ function clamp01(value) {
1862
+ return Math.min(1, Math.max(0, value));
1863
+ }
1864
+ function finiteMs(value) {
1865
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1866
+ }
1867
+ function endTimeOf(animation) {
1868
+ return finiteMs(animation.effect?.getComputedTiming?.().endTime);
1869
+ }
1870
+ function spanOf(animations) {
1871
+ let span = MEASURABLE_SPAN_MS;
1872
+ for (const animation of animations) span = Math.max(span, endTimeOf(animation));
1873
+ return span;
1874
+ }
1875
+ function mergePlayStates(states) {
1876
+ if (states.length === 0) return "idle";
1877
+ if (states.includes("running")) return "running";
1878
+ if (states.includes("paused")) return "paused";
1879
+ return states.every((state) => state === "finished") ? "finished" : "idle";
1880
+ }
1881
+ function quietly(operation) {
1882
+ try {
1883
+ operation();
1884
+ } catch {
1885
+ }
1886
+ }
1887
+ function createCssControl(animations, ledger) {
1888
+ return {
1889
+ pause() {
1890
+ ledger.set("animation-play-state", "paused");
1891
+ },
1892
+ resume() {
1893
+ ledger.set("animation-play-state", "running");
1894
+ },
1895
+ /**
1896
+ * Flip this instance's playback rate and keep running — "turn around", whichever way it was
1897
+ * going.
1898
+ *
1899
+ * No longer the route `ControlHandle.reverse()` takes, and deliberately so: that call is about
1900
+ * the *element's* direction of travel, which only the animator owns, and it needs the absolute
1901
+ * "play out to the from-state" of `EffectInstance.reverse` rather than this relative flip.
1902
+ * Retained because `InstanceControl` is a published interface and this is the honest meaning of
1903
+ * `reverse` at the level of one playhead — a caller holding a single instance's control has no
1904
+ * element-wide direction to speak of.
1905
+ */
1906
+ reverse() {
1907
+ for (const animation of animations()) quietly(() => animation.reverse());
1908
+ ledger.set("animation-play-state", "running");
1909
+ },
1910
+ seek(progress) {
1911
+ const list = animations();
1912
+ const span = spanOf(list);
1913
+ if (span === MEASURABLE_SPAN_MS) return;
1914
+ const at = clamp01(progress) * span;
1915
+ for (const animation of list) {
1916
+ quietly(() => {
1917
+ animation.currentTime = Math.min(at, endTimeOf(animation));
1918
+ });
1919
+ }
1920
+ },
1921
+ rate(playbackRate) {
1922
+ for (const animation of animations()) animation.playbackRate = playbackRate;
1923
+ },
1924
+ get progress() {
1925
+ const list = animations();
1926
+ const span = spanOf(list);
1927
+ if (span === MEASURABLE_SPAN_MS) return 0;
1928
+ let at = 0;
1929
+ for (const animation of list) at = Math.max(at, finiteMs(animation.currentTime));
1930
+ return clamp01(at / span);
1931
+ },
1932
+ get playState() {
1933
+ return mergePlayStates(animations().map((animation) => animation.playState));
1934
+ }
1935
+ };
1936
+ }
1937
+ function bindElement(animator, el) {
1938
+ const state = animator.stateOf(el);
1939
+ if (!state) {
1940
+ return {
1941
+ el,
1942
+ controls: [],
1943
+ unreachable: [],
1944
+ refused: true,
1945
+ note: "no kUInetic effect is installed on this element \u2014 it has no playhead to control"
1946
+ };
1947
+ }
1948
+ if (state.progressDriven) {
1949
+ return {
1950
+ el,
1951
+ controls: [],
1952
+ unreachable: [...state.fxNames],
1953
+ refused: true,
1954
+ note: `"${state.fxNames.join(" ")}" is driven by scroll position rather than a clock, so its playhead belongs to the scroller \u2014 pausing or seeking it would be overwritten on the next frame`
1955
+ };
1956
+ }
1957
+ const controls = state.instances.map((instance) => instance.control).filter((control2) => control2 !== void 0);
1958
+ const unreachable = [...state.jsEffectNames];
1959
+ if (unreachable.length === 0) return { el, controls, unreachable };
1960
+ return {
1961
+ el,
1962
+ controls,
1963
+ unreachable,
1964
+ note: `"${unreachable.join(" ")}" ${unreachable.length === 1 ? "is" : "are"} rendered in JavaScript and expose${unreachable.length === 1 ? "s" : ""} no playhead, so pause, seek and timeScale do not reach ${unreachable.length === 1 ? "it" : "them"}`
1965
+ };
1966
+ }
1967
+ function withDerived(animator, elements) {
1968
+ const seen = /* @__PURE__ */ new Set();
1969
+ const out = [];
1970
+ for (const el of elements) {
1971
+ for (const each of [el, ...animator.derivedOf(el)]) {
1972
+ if (seen.has(each)) continue;
1973
+ seen.add(each);
1974
+ out.push(each);
1975
+ }
1976
+ }
1977
+ return out;
1978
+ }
1979
+ function control(request) {
1980
+ const { animator, root, target } = request;
1981
+ const elements = resolveTargets(target, root);
1982
+ const reach = withDerived(animator, elements);
1983
+ const bounds = reach.map((el) => bindElement(animator, el));
1984
+ for (const [index, bound] of bounds.entries()) {
1985
+ if (bound.note) animator.reporter.warn(`control(): ${bound.note}`, reach[index]);
1986
+ }
1987
+ const each = (operation) => {
1988
+ for (const bound of bounds) for (const instance of bound.controls) operation(instance);
1989
+ return handle;
1990
+ };
1991
+ const reject2 = (call, value) => {
1992
+ animator.reporter.warn(`control(): ${call} ignored \u2014 ${String(value)} is not a finite number`);
1993
+ return handle;
1994
+ };
1995
+ const reverseAll = () => {
1996
+ for (const bound of bounds) if (!bound.refused) animator.reverseFrom(bound.el);
1997
+ return handle;
1998
+ };
1999
+ const handle = {
2000
+ elements,
2001
+ get uncontrolled() {
2002
+ return bounds.flatMap((bound) => bound.unreachable);
2003
+ },
2004
+ pause: () => each((instance) => instance.pause()),
2005
+ play: () => each((instance) => instance.resume()),
2006
+ reverse: () => reverseAll(),
2007
+ seek: (progress) => Number.isFinite(progress) ? each((instance) => instance.seek(progress)) : reject2("seek()", progress),
2008
+ timeScale: (rate) => Number.isFinite(rate) ? each((instance) => instance.rate(rate)) : reject2("timeScale()", rate),
2009
+ get progress() {
2010
+ let lowest = 1;
2011
+ let found = false;
2012
+ for (const bound of bounds) {
2013
+ for (const instance of bound.controls) {
2014
+ lowest = Math.min(lowest, instance.progress);
2015
+ found = true;
2016
+ }
2017
+ }
2018
+ return found ? lowest : 0;
2019
+ },
2020
+ get state() {
2021
+ return mergePlayStates(
2022
+ bounds.flatMap((bound) => bound.controls.map((instance) => instance.playState))
2023
+ );
2024
+ }
2025
+ };
2026
+ return handle;
2027
+ }
2028
+ var KUI_EVENT = {
2029
+ /** The element's effects have started. Fires once per activation. */
2030
+ start: "kui:start",
2031
+ /** Every finite effect on the element has completed. */
2032
+ finish: "kui:finish",
2033
+ /**
2034
+ * The element has finished playing *backwards* and is sitting at its from-state again.
2035
+ *
2036
+ * A separate name rather than a second `kui:finish` with a different `reason`, because the
2037
+ * commonest thing an author does with `kui:finish` is chain the next reveal off it, and a
2038
+ * listener that has to remember to re-check `event.detail.reason` before doing so is a trap
2039
+ * rather than an API — the one time they forget, a hover-out plays the next section in. Filtering
2040
+ * by *name* is what `addEventListener` is for, and it is what a delegated document-level listener
2041
+ * can do without reading the detail at all.
2042
+ *
2043
+ * The exit half of a paired activation (`data-kui-on="pointerenter/pointerleave"`) settles here,
2044
+ * and so does a programmatic `control(el).reverse()`. Before this existed, a completed reverse
2045
+ * dispatched nothing whatsoever, which left "the element is back where it started" the one
2046
+ * lifecycle moment an author could only discover by polling `data-kui-state`.
2047
+ */
2048
+ reverseFinish: "kui:reverse-finish",
2049
+ /** The element's effects were torn down or cancelled before completing. */
2050
+ cancel: "kui:cancel"
2051
+ };
2052
+ function emitLifecycle(el, type, detail) {
2053
+ const Ctor = el.ownerDocument?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
2054
+ if (typeof Ctor !== "function") return;
2055
+ el.dispatchEvent(new Ctor(type, { detail, bubbles: true, composed: true }));
2056
+ }
2057
+
2058
+ // src/core/params.ts
2059
+ var DANGEROUS = /[;<]|\/\*|url\s*\(|expression\s*\(|@import|image-set\s*\(/i;
2060
+ var ABSOLUTE_OR_PROTOCOL_RELATIVE = /^(?:[a-z][a-z0-9+.-]*:|[/\\]{2})/i;
2061
+ var MAX_VALUE_LENGTH = 200;
2062
+ var MAX_PATH_LENGTH = 2e3;
2063
+ var PATH_DATA = /^[MmZzLlHhVvCcSsQqTtAa0-9eE.,+\-\s]+$/;
2064
+ var NUM = String.raw`-?(?:\d+(?:\.\d+)?|\.\d+)`;
2065
+ var LENGTH_UNITS = "px|rem|em|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|q|%";
2066
+ var PATTERNS = {
2067
+ length: new RegExp(`^(?:0|${NUM}(?:${LENGTH_UNITS}))$`, "i"),
2068
+ time: new RegExp(`^${NUM}(?:ms|s)$`, "i"),
2069
+ number: new RegExp(`^${NUM}$`),
2070
+ percentage: new RegExp(`^${NUM}%$`),
2071
+ angle: new RegExp(`^${NUM}(?:deg|rad|turn|grad)$`, "i")
2072
+ };
2073
+ var BARE_ANGLE = new RegExp(`^(${NUM})d?$`, "i");
2074
+ function withAngleUnit(value) {
2075
+ const match = BARE_ANGLE.exec(value);
2076
+ return match ? `${match[1]}deg` : value;
2077
+ }
2078
+ var BARE_NUMBER = /^([+-]?)(\d+(?:\.\d+)?|\.\d+)(?:e([+-]?\d+))?$/i;
2079
+ var HEX_COLOR = /^#[0-9a-f]{3,8}$/i;
2080
+ var COLOR_FUNCTIONS = /^(?:rgba?|hsla?|okl(?:ch|ab)|l(?:ch|ab)|color)\([^()]*\)$/i;
2081
+ var COLOR_KEYWORDS = /* @__PURE__ */ new Set([
2082
+ // Lowercase throughout, matching how the only reader spells the lookup: `value.toLowerCase()`.
2083
+ // CSS keywords are ASCII case-insensitive, so `Canvas`, `canvas` and `CANVAS` are one value.
2084
+ "accentcolor",
2085
+ "accentcolortext",
2086
+ "activetext",
2087
+ "buttonborder",
2088
+ "buttonface",
2089
+ "buttontext",
2090
+ "canvas",
2091
+ "canvastext",
2092
+ "field",
2093
+ "fieldtext",
2094
+ "graytext",
2095
+ "highlight",
2096
+ "highlighttext",
2097
+ "linktext",
2098
+ "mark",
2099
+ "marktext",
2100
+ "selecteditem",
2101
+ "selecteditemtext",
2102
+ "visitedtext",
2103
+ "transparent",
2104
+ "currentcolor",
2105
+ "aliceblue",
2106
+ "antiquewhite",
2107
+ "aqua",
2108
+ "aquamarine",
2109
+ "azure",
2110
+ "beige",
2111
+ "bisque",
2112
+ "black",
2113
+ "blanchedalmond",
2114
+ "blue",
2115
+ "blueviolet",
2116
+ "brown",
2117
+ "burlywood",
2118
+ "cadetblue",
2119
+ "chartreuse",
2120
+ "chocolate",
2121
+ "coral",
2122
+ "cornflowerblue",
2123
+ "cornsilk",
2124
+ "crimson",
2125
+ "cyan",
2126
+ "darkblue",
2127
+ "darkcyan",
2128
+ "darkgoldenrod",
2129
+ "darkgray",
2130
+ "darkgreen",
2131
+ "darkgrey",
2132
+ "darkkhaki",
2133
+ "darkmagenta",
2134
+ "darkolivegreen",
2135
+ "darkorange",
2136
+ "darkorchid",
2137
+ "darkred",
2138
+ "darksalmon",
2139
+ "darkseagreen",
2140
+ "darkslateblue",
2141
+ "darkslategray",
2142
+ "darkslategrey",
2143
+ "darkturquoise",
2144
+ "darkviolet",
2145
+ "deeppink",
2146
+ "deepskyblue",
2147
+ "dimgray",
2148
+ "dimgrey",
2149
+ "dodgerblue",
2150
+ "firebrick",
2151
+ "floralwhite",
2152
+ "forestgreen",
2153
+ "fuchsia",
2154
+ "gainsboro",
2155
+ "ghostwhite",
2156
+ "gold",
2157
+ "goldenrod",
2158
+ "gray",
2159
+ "green",
2160
+ "greenyellow",
2161
+ "grey",
2162
+ "honeydew",
2163
+ "hotpink",
2164
+ "indianred",
2165
+ "indigo",
2166
+ "ivory",
2167
+ "khaki",
2168
+ "lavender",
2169
+ "lavenderblush",
2170
+ "lawngreen",
2171
+ "lemonchiffon",
2172
+ "lightblue",
2173
+ "lightcoral",
2174
+ "lightcyan",
2175
+ "lightgoldenrodyellow",
2176
+ "lightgray",
2177
+ "lightgreen",
2178
+ "lightgrey",
2179
+ "lightpink",
2180
+ "lightsalmon",
2181
+ "lightseagreen",
2182
+ "lightskyblue",
2183
+ "lightslategray",
2184
+ "lightslategrey",
2185
+ "lightsteelblue",
2186
+ "lightyellow",
2187
+ "lime",
2188
+ "limegreen",
2189
+ "linen",
2190
+ "magenta",
2191
+ "maroon",
2192
+ "mediumaquamarine",
2193
+ "mediumblue",
2194
+ "mediumorchid",
2195
+ "mediumpurple",
2196
+ "mediumseagreen",
2197
+ "mediumslateblue",
2198
+ "mediumspringgreen",
2199
+ "mediumturquoise",
2200
+ "mediumvioletred",
2201
+ "midnightblue",
2202
+ "mintcream",
2203
+ "mistyrose",
2204
+ "moccasin",
2205
+ "navajowhite",
2206
+ "navy",
2207
+ "oldlace",
2208
+ "olive",
2209
+ "olivedrab",
2210
+ "orange",
2211
+ "orangered",
2212
+ "orchid",
2213
+ "palegoldenrod",
2214
+ "palegreen",
2215
+ "paleturquoise",
2216
+ "palevioletred",
2217
+ "papayawhip",
2218
+ "peachpuff",
2219
+ "peru",
2220
+ "pink",
2221
+ "plum",
2222
+ "powderblue",
2223
+ "purple",
2224
+ "rebeccapurple",
2225
+ "red",
2226
+ "rosybrown",
2227
+ "royalblue",
2228
+ "saddlebrown",
2229
+ "salmon",
2230
+ "sandybrown",
2231
+ "seagreen",
2232
+ "seashell",
2233
+ "sienna",
2234
+ "silver",
2235
+ "skyblue",
2236
+ "slateblue",
2237
+ "slategray",
2238
+ "slategrey",
2239
+ "snow",
2240
+ "springgreen",
2241
+ "steelblue",
2242
+ "tan",
2243
+ "teal",
2244
+ "thistle",
2245
+ "tomato",
2246
+ "turquoise",
2247
+ "violet",
2248
+ "wheat",
2249
+ "white",
2250
+ "whitesmoke",
2251
+ "yellow",
2252
+ "yellowgreen"
2253
+ ]);
2254
+ var EASING_KEYWORD = /^(?:linear|ease|step-start|step-end|spring|bounce|[a-z]+-(?:in|out|in-out))$/i;
2255
+ var EASING_FUNCTION = /^(?:cubic-bezier|steps|linear|spring)\([^()]*\)$/i;
2256
+ var UNION_GRAMMARS = {
2257
+ "number|percentage": ["number", "percentage"],
2258
+ "length|percentage": ["length", "percentage"],
2259
+ "angle|keyword": ["angle"]
2260
+ };
2261
+ function grammarsFor(type) {
2262
+ return UNION_GRAMMARS[type] ?? [type];
2263
+ }
2264
+ var CALC_TYPES = /* @__PURE__ */ new Set(["length", "percentage", "number"]);
2265
+ var BOUNDED_TYPES = /* @__PURE__ */ new Set(["number", "number|percentage"]);
2266
+ var CALC_CHARACTER = /^[\d.\s+\-*/%a-z,]$/i;
2267
+ var CUSTOM_PROPERTY_NAME = /^--[\w-]+$/;
2268
+ function validate(raw, spec) {
2269
+ const value = raw.trim();
2270
+ const rejection = screen(value, spec);
2271
+ if (rejection) return rejection;
2272
+ const word = checkKeywordHalf(value, spec);
2273
+ if (word) return word;
2274
+ if (spec.type === "text") return { value, ok: true };
2275
+ if (spec.type === "path") return checkPath(value, spec);
2276
+ const typed = normalise(value, spec.type);
2277
+ if (isAcceptable(typed, spec.type)) return checkNumericConstraints(typed, spec);
2278
+ return reject(spec, `not a valid ${describeType(spec)}`);
2279
+ }
2280
+ function checkKeywordHalf(value, spec) {
2281
+ if (spec.type !== "keyword" && spec.type !== "angle|keyword") return null;
2282
+ if (spec.keywords?.includes(value)) return { value, ok: true };
2283
+ if (spec.type !== "keyword") return null;
2284
+ return reject(spec, `expected one of ${spec.keywords?.join(", ") || "(none declared)"}`);
2285
+ }
2286
+ function normalise(value, type) {
2287
+ if (type === "angle" || type === "angle|keyword") return withAngleUnit(value);
2288
+ if (type === "number|percentage") return withoutPercentage(value);
2289
+ return value;
2290
+ }
2291
+ function withoutPercentage(value) {
2292
+ if (!PATTERNS.percentage.test(value)) return value;
2293
+ return decimalNumber(value.slice(0, -1), -2) ?? value;
2294
+ }
2295
+ function decimalNumber(raw, shift = 0) {
2296
+ const match = BARE_NUMBER.exec(raw);
2297
+ if (!match || raw.length > MAX_VALUE_LENGTH) return void 0;
2298
+ const [, sign, coefficient, exponent] = match;
2299
+ const digits = coefficient.replace(".", "");
2300
+ const dot = coefficient.indexOf(".");
2301
+ const position = (dot < 0 ? digits.length : dot) + Number(exponent ?? 0) + shift;
2302
+ if (Math.abs(position) + digits.length > 190) return void 0;
2303
+ const padded = "0".repeat(Math.max(0, -position)) + digits + "0".repeat(Math.max(0, position - digits.length));
2304
+ const split = Math.max(0, position);
2305
+ const integer = padded.slice(0, split).replace(/^0+/, "") || "0";
2306
+ const fraction = fractionalPart(padded, split);
2307
+ return `${sign === "-" ? "-" : ""}${integer}${fraction}`;
2308
+ }
2309
+ function fractionalPart(digits, start) {
2310
+ let end = digits.length;
2311
+ while (end > start && digits[end - 1] === "0") end--;
2312
+ return end > start ? "." + digits.slice(start, end) : "";
2313
+ }
2314
+ function isSafeCssValue(value) {
2315
+ return value.length <= MAX_VALUE_LENGTH && !DANGEROUS.test(value);
2316
+ }
2317
+ function checkPath(value, spec) {
2318
+ if (!PATH_DATA.test(value)) return reject(spec, "path data contains an unsupported character");
2319
+ if (!/^[Mm]/.test(value)) return reject(spec, "path data must start with a moveto (M or m)");
2320
+ return { value: `"${value}"`, ok: true };
2321
+ }
2322
+ function screen(value, spec) {
2323
+ if (!value) return reject(spec, "empty value");
2324
+ const limit = spec.type === "path" ? MAX_PATH_LENGTH : MAX_VALUE_LENGTH;
2325
+ if (value.length > limit) {
2326
+ return reject(spec, `value exceeds ${limit} characters`);
2327
+ }
2328
+ if (DANGEROUS.test(value)) return reject(spec, "value contains disallowed CSS syntax");
2329
+ return null;
2330
+ }
2331
+ function isAcceptable(value, type) {
2332
+ if (type === "color") return isColor(value);
2333
+ if (type === "easing") return EASING_KEYWORD.test(value) || EASING_FUNCTION.test(value);
2334
+ const grammars = grammarsFor(type);
2335
+ if (grammars.some((grammar) => PATTERNS[grammar]?.test(value))) return true;
2336
+ if (!grammars.some((grammar) => CALC_TYPES.has(grammar))) return false;
2337
+ return isSafeCalc(value) && isWellFormedCalc(value);
2338
+ }
2339
+ function describeType(spec) {
2340
+ if (spec.type === "angle|keyword") {
2341
+ return `angle or one of ${spec.keywords?.join(", ") || "(none declared)"}`;
2342
+ }
2343
+ return spec.type.replace("|", " or ");
2344
+ }
2345
+ function isColor(value) {
2346
+ return HEX_COLOR.test(value) || COLOR_FUNCTIONS.test(value) || COLOR_KEYWORDS.has(value.toLowerCase());
2347
+ }
2348
+ function isSameOriginPath(value) {
2349
+ return !ABSOLUTE_OR_PROTOCOL_RELATIVE.test(value);
2350
+ }
2351
+ function checkNumericConstraints(value, spec) {
2352
+ if (!BOUNDED_TYPES.has(spec.type)) return { value, ok: true };
2353
+ const numeric = Number(value);
2354
+ if (spec.finite && !Number.isFinite(numeric)) return reject(spec, "expected a finite number");
2355
+ if (spec.integer && !Number.isInteger(numeric)) return reject(spec, "expected an integer");
2356
+ if (spec.minimum !== void 0 && numeric < spec.minimum) {
2357
+ return reject(spec, `expected at least ${spec.minimum}`);
2358
+ }
2359
+ if (spec.maximum !== void 0 && numeric > spec.maximum) {
2360
+ return reject(spec, `expected at most ${spec.maximum}`);
2361
+ }
2362
+ return { value, ok: true };
2363
+ }
2364
+ function reject(spec, reason) {
2365
+ return { value: spec.default, ok: false, reason };
2366
+ }
2367
+ function isSafeCalc(value) {
2368
+ if (!value.startsWith("calc(") || !value.endsWith(")")) return false;
2369
+ const end = value.length - 1;
2370
+ let index = "calc(".length;
2371
+ while (index < end) {
2372
+ index = nextCalcToken(value, index, end);
2373
+ if (index < 0) return false;
2374
+ }
2375
+ return true;
2376
+ }
2377
+ function nextCalcToken(value, index, end) {
2378
+ if (value.startsWith("var(", index)) return consumeVar(value, index, end);
2379
+ return CALC_CHARACTER.test(value[index]) ? index + 1 : -1;
2380
+ }
2381
+ function consumeVar(value, start, end) {
2382
+ const close = value.indexOf(")", start + "var(".length);
2383
+ if (close < 0 || close >= end) return -1;
2384
+ const name = value.slice(start + "var(".length, close);
2385
+ return CUSTOM_PROPERTY_NAME.test(name) ? close + 1 : -1;
2386
+ }
2387
+ function isWellFormedCalc(value) {
2388
+ const body = value.slice("calc(".length, -1).trim();
2389
+ return body !== "" && !/[+\-*/]$/.test(body);
2390
+ }
2391
+ function resolveParams(authored, schema, warn) {
2392
+ const out = {};
2393
+ for (const [key, raw] of Object.entries(authored)) {
2394
+ const spec = Object.hasOwn(schema, key) ? schema[key] : void 0;
2395
+ if (!spec) {
2396
+ warn(`unknown parameter "${key}" (known: ${Object.keys(schema).join(", ") || "none"})`);
2397
+ continue;
2398
+ }
2399
+ if (spec.type === "text") continue;
2400
+ const result = validate(raw, spec);
2401
+ if (!result.ok) {
2402
+ warn(`parameter "${key}": ${result.reason} \u2014 got "${raw}", using default "${spec.default}"`);
2403
+ continue;
2404
+ }
2405
+ out[spec.cssProperty] = cssValueFor(result.value, spec, key, warn);
2406
+ }
2407
+ return out;
2408
+ }
2409
+ function cssValueFor(value, spec, key, warn) {
2410
+ if (spec.type !== "easing") return value;
2411
+ for (const problem of springTokenProblems(value)) warn(`parameter "${key}": ${problem}`);
2412
+ return cssEasingValue(value);
2413
+ }
2414
+
2415
+ // src/core/js-params.ts
2416
+ var ABSOLUTE_BASIS = {
2417
+ viewportWidth: 0,
2418
+ viewportHeight: 0,
2419
+ percentBasis: 0,
2420
+ fontSize: 16,
2421
+ rootFontSize: 16
2422
+ };
2423
+ var STATIC_UNITS = {
2424
+ px: 1,
2425
+ cm: 96 / 2.54,
2426
+ mm: 96 / 25.4,
2427
+ q: 96 / 101.6,
2428
+ in: 96,
2429
+ pt: 96 / 72,
2430
+ pc: 16
2431
+ };
2432
+ var BASIS_UNITS = {
2433
+ vh: (b) => b.viewportHeight / 100,
2434
+ vw: (b) => b.viewportWidth / 100,
2435
+ vmin: (b) => Math.min(b.viewportWidth, b.viewportHeight) / 100,
2436
+ vmax: (b) => Math.max(b.viewportWidth, b.viewportHeight) / 100,
2437
+ "%": (b) => b.percentBasis / 100,
2438
+ em: (b) => b.fontSize,
2439
+ rem: (b) => b.rootFontSize,
2440
+ ch: (b) => b.fontSize * 0.5,
2441
+ ex: (b) => b.fontSize * 0.5
2442
+ };
2443
+ var NUMBER_WITH_UNIT = /^(-?(?:\d+(?:\.\d+)?|\.\d+))([a-z%]*)$/i;
2444
+ function readParams(authored, schema, warn) {
2445
+ const out = {};
2446
+ for (const [name, spec] of Object.entries(schema)) out[name] = spec.default;
2447
+ for (const [name, raw] of Object.entries(authored)) {
2448
+ const spec = Object.hasOwn(schema, name) ? schema[name] : void 0;
2449
+ if (!spec) {
2450
+ warn(`unknown parameter "${name}" (known: ${Object.keys(schema).join(", ") || "none"})`);
2451
+ continue;
2452
+ }
2453
+ const result = validate(raw, spec);
2454
+ out[name] = result.value;
2455
+ if (!result.ok) {
2456
+ warn(`parameter "${name}": ${result.reason} \u2014 got "${raw}", using default "${spec.default}"`);
2457
+ }
2458
+ }
2459
+ return out;
2460
+ }
2461
+ function toMilliseconds(value, fallback = 0) {
2462
+ const parts = NUMBER_WITH_UNIT.exec(value.trim());
2463
+ if (!parts) return fallback;
2464
+ const amount = Number.parseFloat(parts[1]);
2465
+ const unit = parts[2].toLowerCase();
2466
+ if (unit === "ms") return amount;
2467
+ if (unit === "s") return amount * 1e3;
2468
+ return fallback;
2469
+ }
2470
+ function toPixels(value, basis, fallback = 0) {
2471
+ const parts = NUMBER_WITH_UNIT.exec(value.trim());
2472
+ if (!parts) return fallback;
2473
+ const amount = Number.parseFloat(parts[1]);
2474
+ const unit = parts[2].toLowerCase();
2475
+ if (unit === "") return amount === 0 ? 0 : fallback;
2476
+ const staticFactor = STATIC_UNITS[unit];
2477
+ if (staticFactor !== void 0) return amount * staticFactor;
2478
+ const basisFactor = BASIS_UNITS[unit];
2479
+ return basisFactor ? amount * basisFactor(basis) : fallback;
2480
+ }
2481
+ function toNumber(value, fallback = 0) {
2482
+ const parts = NUMBER_WITH_UNIT.exec(value.trim());
2483
+ if (!parts) return fallback;
2484
+ const amount = Number.parseFloat(parts[1]);
2485
+ const unit = parts[2];
2486
+ if (unit === "") return amount;
2487
+ if (unit === "%") return amount / 100;
2488
+ return fallback;
2489
+ }
2490
+ var EASING_SPEC = { type: "easing", default: "", cssProperty: "--kui-ease" };
2491
+ function timingMs(raw, label, warn) {
2492
+ if (raw === void 0) return void 0;
2493
+ const ms = toMilliseconds(raw, Number.NaN);
2494
+ if (Number.isFinite(ms)) return ms;
2495
+ warn(`${label} "${raw}" is not a valid time \u2014 ignored`);
2496
+ return void 0;
2497
+ }
2498
+ function timingEasing(raw, warn) {
2499
+ if (raw === void 0) return void 0;
2500
+ const result = validate(raw, EASING_SPEC);
2501
+ if (result.ok) return result.value;
2502
+ warn(`easing "${raw}": ${result.reason} \u2014 ignored`);
2503
+ return void 0;
2504
+ }
2505
+ function readEffectTiming(spec, warn) {
2506
+ return {
2507
+ durationMs: timingMs(spec.duration, "duration", warn),
2508
+ delayMs: timingMs(spec.delay, "delay", warn),
2509
+ easing: timingEasing(spec.easing, warn)
2510
+ };
2511
+ }
2512
+ function effectDurationMs(params, fallback) {
2513
+ return params.timing.durationMs ?? params.ms("duration", fallback);
2514
+ }
2515
+ function createParams(values, timing = {}) {
2516
+ return {
2517
+ text: (name, fallback = "") => values[name] ?? fallback,
2518
+ ms: (name, fallback = 0) => toMilliseconds(values[name] ?? "", fallback),
2519
+ num: (name, fallback = 0) => toNumber(values[name] ?? "", fallback),
2520
+ is: (name, value = "true") => (values[name] ?? "") === value,
2521
+ timing
2522
+ };
2523
+ }
2524
+ function readEffectParams(authored, schema, warn, timing = {}) {
2525
+ return createParams(readParams(authored, schema, warn), timing);
2526
+ }
2527
+
2528
+ // src/core/scroll-scheduler.ts
2529
+ function defaultDeps() {
2530
+ const raf = globalThis.requestAnimationFrame;
2531
+ if (typeof raf !== "function") {
2532
+ return {
2533
+ requestFrame: (callback) => globalThis.setTimeout(callback, 16),
2534
+ cancelFrame: (handle) => globalThis.clearTimeout(handle)
2535
+ };
2536
+ }
2537
+ return {
2538
+ requestFrame: (callback) => raf(callback),
2539
+ cancelFrame: (handle) => globalThis.cancelAnimationFrame(handle)
2540
+ };
2541
+ }
2542
+ function createScrollScheduler(deps = defaultDeps()) {
2543
+ const entries = /* @__PURE__ */ new Map();
2544
+ let epoch = 0;
2545
+ let pending = null;
2546
+ function schedule(entry) {
2547
+ if (entry) entry.dirty = true;
2548
+ else for (const other of entries.values()) other.dirty = true;
2549
+ if (pending !== null) return;
2550
+ pending = deps.requestFrame(runFrame);
2551
+ }
2552
+ function runFrame() {
2553
+ pending = null;
2554
+ for (const entry of entries.values()) {
2555
+ if (!entry.dirty || entry.subscribers.size === 0) continue;
2556
+ entry.dirty = false;
2557
+ const frame = { metrics: entry.root.metrics(), epoch };
2558
+ for (const subscriber of [...entry.subscribers]) subscriber(frame);
2559
+ }
2560
+ }
2561
+ function attach(root) {
2562
+ const existing = entries.get(root.key);
2563
+ if (existing) return existing;
2564
+ const entry = { root, subscribers: /* @__PURE__ */ new Set(), detach: [], dirty: false };
2565
+ entry.detach.push(root.onScroll(() => schedule(entry)));
2566
+ entry.detach.push(
2567
+ root.onResize(() => {
2568
+ epoch++;
2569
+ schedule();
2570
+ })
2571
+ );
2572
+ entries.set(root.key, entry);
2573
+ return entry;
2574
+ }
2575
+ function release(entry) {
2576
+ if (entry.subscribers.size > 0) return;
2577
+ for (const detach of entry.detach) detach();
2578
+ entries.delete(entry.root.key);
2579
+ }
2580
+ return {
2581
+ subscribe(root, onFrame) {
2582
+ const entry = attach(root);
2583
+ entry.subscribers.add(onFrame);
2584
+ schedule(entry);
2585
+ return () => {
2586
+ entry.subscribers.delete(onFrame);
2587
+ release(entry);
2588
+ };
2589
+ },
2590
+ invalidate() {
2591
+ epoch++;
2592
+ schedule();
2593
+ },
2594
+ rootCount: () => entries.size,
2595
+ destroy() {
2596
+ if (pending !== null) deps.cancelFrame(pending);
2597
+ pending = null;
2598
+ for (const entry of entries.values()) {
2599
+ entry.subscribers.clear();
2600
+ for (const detach of entry.detach) detach();
2601
+ }
2602
+ entries.clear();
2603
+ }
2604
+ };
2605
+ }
2606
+ function windowScrollRoot(win) {
2607
+ return {
2608
+ key: "window",
2609
+ metrics: () => ({
2610
+ scrollTop: win.scrollY,
2611
+ scrollLeft: win.scrollX,
2612
+ viewportWidth: win.innerWidth,
2613
+ viewportHeight: win.innerHeight,
2614
+ viewportTop: 0,
2615
+ viewportLeft: 0
2616
+ }),
2617
+ onScroll: (handler) => listen(win, "scroll", handler),
2618
+ /*
2619
+ * The document's own height counts as a resize, not just the window's.
2620
+ *
2621
+ * `elementScrollRoot` has observed its element's size since it was written, for the reason
2622
+ * stated three doc comments down: "images loading, fonts swapping, a container changing size,
2623
+ * or content being inserted". Every one of those applies at least as strongly to the page
2624
+ * itself, and the page root was the one that only listened for `window.resize` — so on any
2625
+ * document with lazy-loaded media below the fold, every cached content offset silently drifted
2626
+ * by however much the page grew after it was measured, with no epoch bump to correct it.
2627
+ *
2628
+ * Measured on `demo/scroll.html` (32,000px of lazy screenshots): the horizontal track's stage
2629
+ * moved ~840px between page load and reaching it, which is a third of that effect's whole
2630
+ * scroll range. Nothing was wrong with the maths; the number it was doing the maths on was
2631
+ * taken before forty images existed.
2632
+ */
2633
+ onResize: (handler) => observeSize(win.document.documentElement, win, handler)
2634
+ };
2635
+ }
2636
+ function elementScrollRoot(el, win) {
2637
+ return {
2638
+ key: `el:${rootId(el)}`,
2639
+ metrics: () => {
2640
+ const rect = el.getBoundingClientRect();
2641
+ return {
2642
+ scrollTop: el.scrollTop,
2643
+ scrollLeft: el.scrollLeft,
2644
+ viewportWidth: el.clientWidth,
2645
+ viewportHeight: el.clientHeight,
2646
+ viewportTop: rect.top,
2647
+ viewportLeft: rect.left
2648
+ };
2649
+ },
2650
+ onScroll: (handler) => listen(el, "scroll", handler),
2651
+ // Window resize alone misses the cases that actually invalidate a nested scroller: images
2652
+ // loading, fonts swapping, a container changing size, or content being inserted.
2653
+ onResize: (handler) => observeSize(el, win, handler)
2654
+ };
2655
+ }
2656
+ function listen(target, type, handler) {
2657
+ target.addEventListener(type, handler, { passive: true });
2658
+ return () => target.removeEventListener(type, handler);
2659
+ }
2660
+ function observeSize(el, win, handler) {
2661
+ const stopWindow = listen(win, "resize", handler);
2662
+ if (typeof ResizeObserver === "undefined") return stopWindow;
2663
+ const observer = new ResizeObserver(handler);
2664
+ observer.observe(el);
2665
+ return () => {
2666
+ stopWindow();
2667
+ observer.disconnect();
2668
+ };
2669
+ }
2670
+ var nextRootId = 0;
2671
+ var rootIds = /* @__PURE__ */ new WeakMap();
2672
+ function rootId(el) {
2673
+ const existing = rootIds.get(el);
2674
+ if (existing) return existing;
2675
+ const id = String(++nextRootId);
2676
+ rootIds.set(el, id);
2677
+ return id;
2678
+ }
2679
+ function createRootResolver(options) {
2680
+ const { win } = options;
2681
+ const isScrollable = options.isScrollable ?? defaultIsScrollable(win);
2682
+ const windowRoot = windowScrollRoot(win);
2683
+ return (el) => {
2684
+ for (let node = el.parentElement; node; node = node.parentElement) {
2685
+ if (isScrollable(node)) return elementScrollRoot(node, win);
2686
+ }
2687
+ return windowRoot;
2688
+ };
2689
+ }
2690
+ var SCROLLABLE_OVERFLOW = /* @__PURE__ */ new Set(["auto", "scroll", "overlay"]);
2691
+ function defaultIsScrollable(win) {
2692
+ return (el) => {
2693
+ const style = win.getComputedStyle(el);
2694
+ const vertical = SCROLLABLE_OVERFLOW.has(style.overflowY) && el.scrollHeight > el.clientHeight;
2695
+ const horizontal = SCROLLABLE_OVERFLOW.has(style.overflowX) && el.scrollWidth > el.clientWidth;
2696
+ return vertical || horizontal;
2697
+ };
2698
+ }
2699
+ function clamp012(value) {
2700
+ if (Number.isNaN(value)) return 0;
2701
+ return Math.min(1, Math.max(0, value));
2702
+ }
2703
+ function createMeasureCache(measure) {
2704
+ let cachedEpoch = -1;
2705
+ let cached;
2706
+ return {
2707
+ read(epoch) {
2708
+ if (cachedEpoch !== epoch || cached === void 0) {
2709
+ cached = measure();
2710
+ cachedEpoch = epoch;
2711
+ }
2712
+ return cached;
2713
+ },
2714
+ clear() {
2715
+ cachedEpoch = -1;
2716
+ cached = void 0;
2717
+ }
2718
+ };
2719
+ }
2720
+
2721
+ // src/core/time-scale.ts
2722
+ var TIME_SCALE_ATTR = "data-kui-time-scale";
2723
+ function timeScaleOf(el) {
2724
+ const host = el.closest(`[${TIME_SCALE_ATTR}]`);
2725
+ const value = host?.getAttribute(TIME_SCALE_ATTR)?.trim();
2726
+ if (!value) return 1;
2727
+ const parsed = Number(value);
2728
+ if (!Number.isFinite(parsed)) return 1;
2729
+ return Math.min(4, Math.max(0.05, parsed));
2730
+ }
2731
+
2732
+ // src/core/instances.ts
2733
+ function animationsOf(el) {
2734
+ const getAnimations = el.getAnimations;
2735
+ return typeof getAnimations === "function" ? getAnimations.call(el) : [];
2736
+ }
2737
+ function ownedAnimationsOf(el, names) {
2738
+ return animationsOf(el).filter((animation) => {
2739
+ const name = animation.animationName;
2740
+ return typeof name === "string" && names.has(name);
2741
+ });
2742
+ }
2743
+ function flushComputedAnimationName(el) {
2744
+ return getComputedStyle(el).animationName;
2745
+ }
2746
+ function restartCssAnimation(el, ledger) {
2747
+ const name = el.style.getPropertyValue("animation-name");
2748
+ ledger.set("animation-name", "none");
2749
+ flushComputedAnimationName(el);
2750
+ ledger.set("animation-name", name);
2751
+ }
2752
+ function watchCompletion(animations, settle) {
2753
+ if (animations.length === 0) {
2754
+ settle();
2755
+ return;
2756
+ }
2757
+ void Promise.all(animations.map((a) => a.finished.catch(() => void 0))).then(() => settle());
2758
+ }
2759
+ function startCssAnimations(el, ledger, ownedNames, activatedBefore) {
2760
+ let animations = ownedAnimationsOf(el, ownedNames);
2761
+ if (!activatedBefore) {
2762
+ restartCssAnimation(el, ledger);
2763
+ ledger.set("animation-play-state", "running");
2764
+ animations = ownedAnimationsOf(el, ownedNames);
2765
+ const scale = timeScaleOf(el);
2766
+ if (scale !== 1) {
2767
+ for (const animation of animations) animation.playbackRate = scale;
2768
+ }
2769
+ return animations;
2770
+ }
2771
+ const stale = animations.filter((animation) => animation.playState === "finished");
2772
+ if (stale.length > 0) {
2773
+ for (const animation of stale) animation.reverse();
2774
+ } else {
2775
+ ledger.set("animation-play-state", "running");
2776
+ }
2777
+ return animations;
2778
+ }
2779
+ function createCssInstance(el, ledger, animationNames, scrubbed = false) {
2780
+ const ownedNames = new Set(animationNames);
2781
+ let settle;
2782
+ let finished = new Promise((resolve) => {
2783
+ settle = resolve;
2784
+ });
2785
+ let activatedBefore = false;
2786
+ const watch = (animations) => watchCompletion(animations, () => settle?.());
2787
+ function drive(rate) {
2788
+ if (scrubbed) return;
2789
+ const animations = ownedAnimationsOf(el, ownedNames);
2790
+ finished = new Promise((resolve) => {
2791
+ settle = resolve;
2792
+ });
2793
+ for (const animation of animations) {
2794
+ animation.playbackRate = rate * timeScaleOf(el);
2795
+ animation.play();
2796
+ }
2797
+ ledger.set("animation-play-state", "running");
2798
+ watch(animations);
2799
+ }
2800
+ return {
2801
+ // Attached unconditionally, including for a scrubbed instance. Whether control is *allowed*
2802
+ // here is a policy question about the element's timeline, not a capability question about this
2803
+ // instance — a scrubbed animation has a perfectly real playhead, it just belongs to the
2804
+ // scroller. Keeping that decision in one place (`InstanceState.progressDriven`, read by
2805
+ // `control.ts`) beats splitting it across both files and letting them drift.
2806
+ control: createCssControl(() => ownedAnimationsOf(el, ownedNames), ledger),
2807
+ activate() {
2808
+ finished = new Promise((resolve) => {
2809
+ settle = resolve;
2810
+ });
2811
+ if (scrubbed) {
2812
+ settle?.();
2813
+ return;
2814
+ }
2815
+ const animations = startCssAnimations(el, ledger, ownedNames, activatedBefore);
2816
+ activatedBefore = true;
2817
+ watch(animations);
2818
+ },
2819
+ play() {
2820
+ drive(1);
2821
+ },
2822
+ reverse() {
2823
+ drive(-1);
2824
+ },
2825
+ cancel() {
2826
+ for (const animation of ownedAnimationsOf(el, ownedNames)) animation.cancel();
2827
+ settle?.();
2828
+ },
2829
+ finish() {
2830
+ for (const animation of ownedAnimationsOf(el, ownedNames)) animation.finish();
2831
+ settle?.();
2832
+ },
2833
+ get finished() {
2834
+ return finished;
2835
+ },
2836
+ destroy() {
2837
+ settle?.();
2838
+ }
2839
+ };
2840
+ }
2841
+ function continuousSetup(cleanup) {
2842
+ return { cleanup, continuous: true };
2843
+ }
2844
+ function cleanupOf(result) {
2845
+ if (result === void 0) return void 0;
2846
+ return typeof result === "function" ? result : result.cleanup;
2847
+ }
2848
+ function deferredInstance(setup) {
2849
+ let running;
2850
+ let settle;
2851
+ let finished = Promise.resolve();
2852
+ const teardown = () => {
2853
+ cleanupOf(running)?.();
2854
+ running = void 0;
2855
+ settle?.();
2856
+ };
2857
+ return createJsInstance({
2858
+ activate() {
2859
+ if (running !== void 0) teardown();
2860
+ running = setup();
2861
+ if (typeof running === "function" || "continuous" in running) return;
2862
+ const work = running.finished;
2863
+ finished = new Promise((resolve) => {
2864
+ settle = resolve;
2865
+ });
2866
+ void work.then(() => settle?.());
2867
+ },
2868
+ cancel: teardown,
2869
+ finish() {
2870
+ if (typeof running === "object" && "finish" in running) running.finish?.();
2871
+ settle?.();
2872
+ },
2873
+ destroy: teardown,
2874
+ finished: () => finished,
2875
+ // Only an explicit marker counts. A bare `Cleanup` deliberately does not — see
2876
+ // `ContinuousSetup` above for why the two cannot be told apart by shape.
2877
+ continuous: () => typeof running === "object" && "continuous" in running
2878
+ });
2879
+ }
2880
+ function deferPrepare(setup) {
2881
+ return (...args) => deferredInstance(() => setup(...args));
2882
+ }
2883
+ function createJsInstance(hooks) {
2884
+ let active = false;
2885
+ let run = 0;
2886
+ return {
2887
+ activate() {
2888
+ if (active) return;
2889
+ hooks.activate();
2890
+ active = true;
2891
+ const id = ++run;
2892
+ if (hooks.continuous?.()) return;
2893
+ void (hooks.finished?.() ?? Promise.resolve()).then(() => {
2894
+ if (id === run) active = false;
2895
+ });
2896
+ },
2897
+ cancel() {
2898
+ hooks.cancel?.();
2899
+ active = false;
2900
+ },
2901
+ finish() {
2902
+ hooks.finish?.();
2903
+ },
2904
+ get finished() {
2905
+ return hooks.finished?.() ?? Promise.resolve();
2906
+ },
2907
+ get continuous() {
2908
+ return hooks.continuous?.() ?? false;
2909
+ },
2910
+ destroy() {
2911
+ active = false;
2912
+ hooks.destroy();
2913
+ }
2914
+ };
2915
+ }
2916
+
2917
+ export {
2918
+ selectorBreadth,
2919
+ resolveTarget,
2920
+ SCOPE_PARAM,
2921
+ scopeParam,
2922
+ queryScoped,
2923
+ resolveActivationSpec,
2924
+ startKindOf,
2925
+ isOneShot,
2926
+ createActivationBinder,
2927
+ warnAboutActivation,
2928
+ ATTR,
2929
+ gatedAnimationName,
2930
+ gateMatches,
2931
+ gatesOverlap,
2932
+ breakpointsIn,
2933
+ createGateWatcher,
2934
+ DEFAULT_SPRING,
2935
+ createSpringRunner,
2936
+ defaultSpringDeps,
2937
+ cssEasingValue,
2938
+ waapiEasingValue,
2939
+ resolvePlayback,
2940
+ repeatCount,
2941
+ isInfiniteRepeat,
2942
+ directionValue,
2943
+ playbackExpression,
2944
+ parseToggleActions,
2945
+ warnAboutToggleActions,
2946
+ applyToggleVerb,
2947
+ CHANNEL,
2948
+ inertInstance,
2949
+ SEGMENT_HOIST_KEYS,
2950
+ splitTopLevel,
2951
+ parseActivationAttribute,
2952
+ parse,
2953
+ assignOnce,
2954
+ Registry,
2955
+ timingProperty,
2956
+ suggest,
2957
+ resolveTargets,
2958
+ toAttributeValue,
2959
+ play,
2960
+ control,
2961
+ KUI_EVENT,
2962
+ emitLifecycle,
2963
+ validate,
2964
+ decimalNumber,
2965
+ isSafeCssValue,
2966
+ isSameOriginPath,
2967
+ resolveParams,
2968
+ ABSOLUTE_BASIS,
2969
+ toMilliseconds,
2970
+ toPixels,
2971
+ readEffectTiming,
2972
+ effectDurationMs,
2973
+ readEffectParams,
2974
+ createScrollScheduler,
2975
+ createRootResolver,
2976
+ clamp012 as clamp01,
2977
+ createMeasureCache,
2978
+ TIME_SCALE_ATTR,
2979
+ timeScaleOf,
2980
+ createCssInstance,
2981
+ continuousSetup,
2982
+ deferredInstance,
2983
+ deferPrepare
2984
+ };