kuinetic 0.1.4 → 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 (124) 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 +15147 -6039
  13. package/dist/kuinetic.all.min.js +2 -0
  14. package/dist/kuinetic.css +2801 -318
  15. package/dist/kuinetic.js +15147 -6038
  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 +699 -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/discrete.d.ts +13 -0
  83. package/dist/types/effects/catalog/interaction-proximity.d.ts +24 -0
  84. package/dist/types/effects/catalog/interaction-reveal.d.ts +248 -0
  85. package/dist/types/effects/catalog/interaction-shared.d.ts +42 -0
  86. package/dist/types/effects/catalog/interaction-states.d.ts +68 -0
  87. package/dist/types/effects/catalog/materials.d.ts +67 -0
  88. package/dist/types/effects/catalog/media-shared.d.ts +3 -2
  89. package/dist/types/effects/catalog/numbers-shared.d.ts +8 -5
  90. package/dist/types/effects/catalog/shared.d.ts +1 -1
  91. package/dist/types/effects/catalog/text-shared.d.ts +86 -10
  92. package/dist/types/effects/catalog/transforms.d.ts +13 -0
  93. package/dist/types/effects/catalog/view-transitions.d.ts +13 -0
  94. package/dist/types/effects/forms/primitives.d.ts +15 -4
  95. package/dist/types/effects/gestures/index.d.ts +53 -0
  96. package/dist/types/effects/index.d.ts +5 -0
  97. package/dist/types/effects/layout/presets.d.ts +35 -0
  98. package/dist/types/effects/motion-path/index.d.ts +35 -0
  99. package/dist/types/effects/scroll-mechanics/params.d.ts +23 -0
  100. package/dist/types/effects/scroll-mechanics/presets.d.ts +51 -0
  101. package/dist/types/effects/scroll-mechanics/section-index.d.ts +47 -0
  102. package/dist/types/effects/shared.d.ts +124 -4
  103. package/dist/types/effects/step-index.d.ts +121 -0
  104. package/dist/types/effects/step-marking.d.ts +41 -33
  105. package/dist/types/effects/svg/icon-parts.d.ts +23 -0
  106. package/dist/types/effects/three-d/flip-parts.d.ts +46 -0
  107. package/dist/types/effects/tween/index.d.ts +57 -0
  108. package/dist/types/effects/tween/properties.d.ts +107 -0
  109. package/dist/types/effects/tween/waypoints.d.ts +106 -0
  110. package/dist/types/showcase/compare.d.ts +4 -0
  111. package/dist/types/showcase/device-frame.d.ts +3 -0
  112. package/dist/types/showcase/hotspots.d.ts +3 -0
  113. package/dist/types/showcase/index.d.ts +30 -0
  114. package/dist/types/showcase/lightbox.d.ts +3 -0
  115. package/dist/types/showcase/media-source.d.ts +13 -0
  116. package/dist/types/showcase/modal-shell.d.ts +22 -0
  117. package/dist/types/showcase/scroll-story.d.ts +4 -0
  118. package/dist/types/showcase/shared.d.ts +47 -0
  119. package/dist/types/showcase/slideshow.d.ts +3 -0
  120. package/dist/types/showcase/slow-mo.d.ts +5 -0
  121. package/package.json +14 -7
  122. package/dist/esm/chunk-7WMNPIOZ.mjs +0 -4883
  123. package/dist/esm/chunk-JT4PZL3A.mjs +0 -773
  124. package/dist/esm/chunk-TJDDIQRG.mjs +0 -1290
@@ -1,4883 +0,0 @@
1
- import {
2
- ABSOLUTE_BASIS,
3
- CHANNEL,
4
- Registry,
5
- clamp01,
6
- continuousSetup,
7
- createAttributeLedger,
8
- createMeasureCache,
9
- createStyleLedger,
10
- deferPrepare,
11
- effectDurationMs,
12
- inertInstance,
13
- isSameOriginPath,
14
- toPixels
15
- } from "./chunk-JT4PZL3A.mjs";
16
-
17
- // src/effects/shared.ts
18
- var TRIGGER_DELAY_PARAM = {
19
- delay: { type: "time", default: "0ms", cssProperty: "--kui-delay" }
20
- };
21
- var TIMELINE_AGNOSTIC = ["time", "view", "scroll", "pin"];
22
- var COMMON = {
23
- duration: { type: "time", default: "600ms", cssProperty: "--kui-duration" },
24
- ...TRIGGER_DELAY_PARAM,
25
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
26
- stagger: { type: "time", default: "0ms", cssProperty: "--kui-stagger" }
27
- };
28
- function cssPrimitive(id, channels, options = {}) {
29
- return {
30
- id,
31
- renderer: "css-keyframes",
32
- channels,
33
- parameters: { ...COMMON, ...options.parameters },
34
- supportedTimelines: options.timelines ?? ["time"],
35
- supportedActivations: options.activations ?? [
36
- "load",
37
- "enter",
38
- "hover",
39
- "focus",
40
- "click",
41
- "manual"
42
- ],
43
- perfClass: options.perfClass ?? "compositor",
44
- reducedMotion: options.reducedMotion ?? "shorten",
45
- ...options.defaultActivation ? { defaultActivation: options.defaultActivation } : {}
46
- };
47
- }
48
-
49
- // src/effects/catalog/ambient.ts
50
- var drift = {
51
- duration: { type: "time", default: "10s", cssProperty: "--kui-duration" },
52
- ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
53
- from: { type: "color", default: "", cssProperty: "--kui-ambient-c1" },
54
- to: { type: "color", default: "", cssProperty: "--kui-ambient-c2" }
55
- };
56
- var tint = {
57
- duration: { type: "time", default: "10s", cssProperty: "--kui-duration" },
58
- ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
59
- from: { type: "color", default: "", cssProperty: "--kui-ambient-c1" }
60
- };
61
- var float = {
62
- duration: { type: "time", default: "4s", cssProperty: "--kui-duration" },
63
- ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
64
- distance: { type: "length", default: "14px", cssProperty: "--kui-distance" }
65
- };
66
- var orbit = {
67
- duration: { type: "time", default: "3.5s", cssProperty: "--kui-duration" },
68
- // `linear` by default, unlike every other ambient primitive: a continuous rotation that eases
69
- // visibly stutters once per revolution, because the ease restarts at each iteration boundary.
70
- ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" },
71
- angle: { type: "angle", default: "360deg", cssProperty: "--kui-to-angle" }
72
- };
73
- var pulse = {
74
- duration: { type: "time", default: "2.2s", cssProperty: "--kui-duration" },
75
- ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
76
- scale: { type: "number", default: "1.15", cssProperty: "--kui-pulse-scale", finite: true, minimum: 1 }
77
- };
78
- var AMBIENT_PRIMITIVES = [
79
- cssPrimitive("ambient-gradient", [CHANNEL.background], {
80
- parameters: drift,
81
- defaultActivation: "load",
82
- reducedMotion: "disable",
83
- perfClass: "continuous"
84
- }),
85
- /*
86
- * `gradient-rotate-border` and `gradient-border` are not background fills, and a second
87
- * primitive is how this catalog says so — the same reason `ambient-tint` exists beside
88
- * `ambient-gradient` above. Channels are per-primitive, so a different channel set means a
89
- * different primitive; folding these into `ambient-gradient` would make `gradient-mesh` and
90
- * `aurora` claim a mask and a box they never touch, and `aurora, pin` would start reporting a
91
- * conflict that isn't there.
92
- *
93
- * What the ring rules actually write (`ambient.css`) beyond the gradient: `mask` +
94
- * `mask-composite`, which subtract the element's own content box to leave a ring — the same
95
- * physical property `media-mask` claims under the `'mask'` channel — and `position: relative`
96
- * plus a `padding` that *is* the ring's thickness, which is a claim on the host's box in the
97
- * sense `pin` and `background-media` already use `'layout'` for. Declared only as
98
- * `background`, a `gradient-border, pin-section` pair composed silently while both decided the
99
- * host's `position`, and `gradient-border, mask-reveal` while both wrote `mask`.
100
- */
101
- cssPrimitive("ambient-gradient-ring", [CHANNEL.background, "mask", "layout"], {
102
- parameters: drift,
103
- defaultActivation: "load",
104
- reducedMotion: "disable",
105
- perfClass: "continuous"
106
- }),
107
- cssPrimitive("ambient-tint", [CHANNEL.background], {
108
- parameters: tint,
109
- defaultActivation: "load",
110
- reducedMotion: "disable",
111
- perfClass: "continuous"
112
- }),
113
- cssPrimitive("ambient-float", [CHANNEL.translate], {
114
- parameters: float,
115
- defaultActivation: "load",
116
- reducedMotion: "disable",
117
- perfClass: "continuous"
118
- }),
119
- cssPrimitive("ambient-orbit", [CHANNEL.rotate], {
120
- parameters: orbit,
121
- defaultActivation: "load",
122
- reducedMotion: "disable",
123
- perfClass: "continuous"
124
- }),
125
- cssPrimitive("ambient-pulse", [CHANNEL.scale, CHANNEL.opacity], {
126
- parameters: pulse,
127
- defaultActivation: "load",
128
- reducedMotion: "disable",
129
- perfClass: "continuous"
130
- })
131
- ];
132
- var AMBIENT_PRESETS = [
133
- { name: "gradient-mesh", primitive: "ambient-gradient", keyframes: "kui-gradient-mesh" },
134
- { name: "aurora", primitive: "ambient-gradient", keyframes: "kui-aurora" },
135
- {
136
- name: "gradient-rotate-border",
137
- primitive: "ambient-gradient-ring",
138
- keyframes: "kui-gradient-rotate-border",
139
- params: { duration: "6s", ease: "linear" }
140
- },
141
- {
142
- // Not `gradient`: this rule masks its own content box away (`mask-composite: exclude`) to leave
143
- // a ring, so putting the name on real content deletes the content. `-border` says that out loud,
144
- // matching `gradient-rotate-border` and `beam-border`.
145
- name: "gradient-border",
146
- primitive: "ambient-gradient-ring",
147
- keyframes: "kui-gradient-border",
148
- params: { duration: "6s", ease: "linear" }
149
- },
150
- {
151
- name: "noise-overlay",
152
- primitive: "ambient-tint",
153
- keyframes: "kui-noise-overlay",
154
- params: { duration: "650ms", ease: "steps(6)" }
155
- },
156
- {
157
- name: "scanline",
158
- primitive: "ambient-tint",
159
- keyframes: "kui-scanline",
160
- params: { duration: "3.5s", ease: "linear" }
161
- },
162
- {
163
- name: "dot-grid-drift",
164
- primitive: "ambient-tint",
165
- keyframes: "kui-dot-grid-drift",
166
- params: { duration: "16s", ease: "linear" }
167
- },
168
- {
169
- name: "line-grid-drift",
170
- primitive: "ambient-tint",
171
- keyframes: "kui-line-grid-drift",
172
- params: { duration: "16s", ease: "linear" }
173
- },
174
- {
175
- name: "starfield",
176
- primitive: "ambient-tint",
177
- keyframes: "kui-starfield",
178
- params: { duration: "40s", ease: "linear" }
179
- },
180
- {
181
- name: "spotlight-follow",
182
- primitive: "ambient-tint",
183
- keyframes: "kui-spotlight-follow",
184
- params: { duration: "9s" }
185
- },
186
- {
187
- name: "wave-blob",
188
- primitive: "ambient-tint",
189
- keyframes: "kui-wave-blob",
190
- params: { duration: "12s" }
191
- },
192
- { name: "float", primitive: "ambient-float", keyframes: "kui-float" },
193
- {
194
- name: "bob",
195
- primitive: "ambient-float",
196
- keyframes: "kui-bob",
197
- params: { duration: "2s", distance: "8px" }
198
- },
199
- {
200
- name: "floating-shapes",
201
- primitive: "ambient-float",
202
- keyframes: "kui-floating-shapes",
203
- params: { duration: "6s", distance: "10px" }
204
- },
205
- { name: "orbit", primitive: "ambient-orbit", keyframes: "kui-orbit" },
206
- { name: "glow-pulse", primitive: "ambient-pulse", keyframes: "kui-glow-pulse" }
207
- ];
208
- function registerAmbient(registry) {
209
- return registry.registerPrimitives(AMBIENT_PRIMITIVES).registerPresets(AMBIENT_PRESETS);
210
- }
211
-
212
- // src/effects/catalog/feedback.ts
213
- var loop = {
214
- duration: { type: "time", default: "1.6s", cssProperty: "--kui-duration" },
215
- ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
216
- };
217
- var spin = {
218
- duration: { type: "time", default: "900ms", cssProperty: "--kui-duration" },
219
- ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
220
- };
221
- var dotPulse = {
222
- duration: { type: "time", default: "1.2s", cssProperty: "--kui-duration" },
223
- ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
224
- dotSize: { type: "length", default: "8px", cssProperty: "--kui-dot-size" }
225
- };
226
- var progressTrack = {
227
- duration: { type: "time", default: "1.4s", cssProperty: "--kui-duration" },
228
- ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" }
229
- };
230
- var toast = {
231
- duration: { type: "time", default: "420ms", cssProperty: "--kui-duration" },
232
- ease: { type: "easing", default: "back-out", cssProperty: "--kui-ease" },
233
- distance: { type: "length", default: "24px", cssProperty: "--kui-distance" }
234
- };
235
- var shake = {
236
- duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
237
- ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
238
- };
239
- var pop = {
240
- duration: { type: "time", default: "420ms", cssProperty: "--kui-duration" },
241
- ease: { type: "easing", default: "back-out", cssProperty: "--kui-ease" },
242
- scale: { type: "number", default: "1.18", cssProperty: "--kui-pop-scale", finite: true, minimum: 1 }
243
- };
244
- var ripple = {
245
- duration: { type: "time", default: "600ms", cssProperty: "--kui-duration" },
246
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
247
- spread: { type: "number", default: "4", cssProperty: "--kui-ripple-scale", finite: true, minimum: 1 }
248
- };
249
- var confirm = {
250
- duration: { type: "time", default: "1400ms", cssProperty: "--kui-duration" },
251
- ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
252
- };
253
- var pull = {
254
- distance: { type: "length", default: "36px", cssProperty: "--kui-pull-distance" }
255
- };
256
- var FEEDBACK_PRIMITIVES = [
257
- cssPrimitive("feedback-shimmer", [CHANNEL.background], {
258
- parameters: loop,
259
- defaultActivation: "load",
260
- reducedMotion: "disable",
261
- perfClass: "continuous"
262
- }),
263
- cssPrimitive("feedback-fade", [CHANNEL.opacity], {
264
- defaultActivation: "manual"
265
- }),
266
- cssPrimitive("feedback-spin", [CHANNEL.rotate], {
267
- parameters: spin,
268
- defaultActivation: "load",
269
- reducedMotion: "disable",
270
- perfClass: "continuous"
271
- }),
272
- // Declares every channel the preset's CSS actually paints, not just what the shared keyframe
273
- // animates: spinner-dots' `[data-kui-fx~='spinner-dots']` rule also sets `background:
274
- // currentColor` (the dot itself) and `box-shadow` (the other two dots), entirely outside
275
- // `@keyframes kui-spinner-dots`. Declaring only [scale, opacity] let a composed
276
- // `background`-writing effect (e.g. gradient-mesh) pass channel-collision detection and then
277
- // have its gradient silently overwritten by this rule — see css-invariants.test.ts's "CSS
278
- // static rules" describe block, which now catches this class of omission directly.
279
- cssPrimitive("feedback-dot-pulse", [CHANNEL.scale, CHANNEL.opacity, CHANNEL.background, "shadow"], {
280
- parameters: dotPulse,
281
- defaultActivation: "load",
282
- reducedMotion: "disable",
283
- perfClass: "continuous"
284
- }),
285
- // Same shape as feedback-dot-pulse above: `[data-kui-fx~='progress-indeterminate']` sets
286
- // `background: currentColor` unconditionally for the bar itself, outside the keyframe.
287
- cssPrimitive("feedback-progress-track", [CHANNEL.translate, CHANNEL.scale, CHANNEL.background], {
288
- parameters: progressTrack,
289
- defaultActivation: "load",
290
- reducedMotion: "disable",
291
- perfClass: "continuous"
292
- }),
293
- cssPrimitive("feedback-toast", [CHANNEL.opacity, CHANNEL.translate], {
294
- parameters: toast,
295
- defaultActivation: "manual"
296
- }),
297
- cssPrimitive("feedback-shake", [CHANNEL.translate], {
298
- parameters: shake,
299
- defaultActivation: "manual"
300
- }),
301
- cssPrimitive("feedback-wobble", [CHANNEL.translate, CHANNEL.rotate], {
302
- defaultActivation: "click"
303
- }),
304
- // `[data-kui-fx~='ripple']` sets `background: currentColor` unconditionally for the ripple
305
- // disc itself, outside the keyframe — same gap as feedback-dot-pulse/feedback-progress-track.
306
- cssPrimitive("feedback-ripple", [CHANNEL.scale, CHANNEL.opacity, CHANNEL.background], {
307
- parameters: ripple,
308
- defaultActivation: "click"
309
- }),
310
- cssPrimitive("feedback-pop", [CHANNEL.scale], {
311
- parameters: pop,
312
- defaultActivation: "manual"
313
- }),
314
- // Declares every channel any preset built on it actually paints, not just what the shared
315
- // keyframes animate: heart-burst's CSS also sets `color`, confetti-burst's also sets
316
- // `background-image`. Understating this let a composed `background`-writing effect (e.g.
317
- // gradient-mesh) pass channel-collision detection and silently overwrite confetti-burst's dots.
318
- cssPrimitive("feedback-burst", [CHANNEL.scale, CHANNEL.opacity, CHANNEL.background, CHANNEL.color], {
319
- parameters: pop,
320
- defaultActivation: "click"
321
- }),
322
- cssPrimitive("feedback-confirm", [CHANNEL.opacity], {
323
- parameters: confirm,
324
- defaultActivation: "click"
325
- }),
326
- cssPrimitive("feedback-pull", [CHANNEL.translate], {
327
- parameters: pull,
328
- defaultActivation: "manual"
329
- })
330
- ];
331
- var FEEDBACK_PRESETS = [
332
- { name: "skeleton-shimmer", primitive: "feedback-shimmer", keyframes: "kui-skeleton-shimmer" },
333
- { name: "skeleton-to-content", primitive: "feedback-fade", keyframes: "kui-skeleton-to-content" },
334
- { name: "spinner", primitive: "feedback-spin", keyframes: "kui-spinner-spin" },
335
- { name: "spinner-dots", primitive: "feedback-dot-pulse", keyframes: "kui-spinner-dots" },
336
- { name: "spinner-ring", primitive: "feedback-spin", keyframes: "kui-spinner-ring-spin" },
337
- {
338
- name: "progress-indeterminate",
339
- primitive: "feedback-progress-track",
340
- keyframes: "kui-progress-indeterminate"
341
- },
342
- { name: "toast-slide-in", primitive: "feedback-toast", keyframes: "kui-toast-slide-in" },
343
- {
344
- name: "toast-slide-out",
345
- primitive: "feedback-toast",
346
- keyframes: "kui-toast-slide-out",
347
- params: { ease: "ease-in" }
348
- },
349
- { name: "shake-error", primitive: "feedback-shake", keyframes: "kui-shake-error" },
350
- {
351
- name: "wobble",
352
- primitive: "feedback-wobble",
353
- keyframes: "kui-wobble",
354
- params: { duration: "600ms", ease: "ease-in-out" }
355
- },
356
- { name: "ripple", primitive: "feedback-ripple", keyframes: "kui-ripple" },
357
- { name: "badge-pop", primitive: "feedback-pop", keyframes: "kui-badge-pop" },
358
- {
359
- name: "count-bump",
360
- primitive: "feedback-pop",
361
- keyframes: "kui-count-bump",
362
- params: { duration: "280ms", scale: "1.3" }
363
- },
364
- {
365
- name: "heart-burst",
366
- primitive: "feedback-burst",
367
- keyframes: "kui-heart-burst",
368
- params: { duration: "700ms", scale: "1.4" }
369
- },
370
- {
371
- name: "confetti-burst",
372
- primitive: "feedback-burst",
373
- keyframes: "kui-confetti-burst",
374
- params: { duration: "900ms", scale: "1.15" }
375
- },
376
- { name: "copy-confirm", primitive: "feedback-confirm", keyframes: "kui-copy-confirm" },
377
- {
378
- name: "pull-to-refresh",
379
- primitive: "feedback-pull",
380
- keyframes: "kui-pull-to-refresh",
381
- params: { duration: "900ms", ease: "ease-out" }
382
- }
383
- ];
384
- function registerFeedback(registry) {
385
- return registry.registerPrimitives(FEEDBACK_PRIMITIVES).registerPresets(FEEDBACK_PRESETS);
386
- }
387
-
388
- // src/core/spring.ts
389
- var DEFAULT_SPRING = {
390
- stiffness: 180,
391
- damping: 24,
392
- mass: 1,
393
- restVelocity: 0.05,
394
- restDisplacement: 0.05
395
- };
396
- var SUBSTEP = 1 / 240;
397
- var MAX_STEP = 0.25;
398
- function stepSpring(state, target, config, dt) {
399
- let { value, velocity } = state;
400
- let remaining = Math.min(Math.max(dt, 0), MAX_STEP);
401
- while (remaining > 0) {
402
- const step = Math.min(SUBSTEP, remaining);
403
- const force = -config.stiffness * (value - target) - config.damping * velocity;
404
- velocity += force / config.mass * step;
405
- value += velocity * step;
406
- remaining -= step;
407
- }
408
- return { value, velocity };
409
- }
410
- function isSettled(state, target, config) {
411
- return Math.abs(state.velocity) < config.restVelocity && Math.abs(state.value - target) < config.restDisplacement;
412
- }
413
- var MAX_SETTLE_MS = 1e4;
414
- function createSpringRunner(config, onChange, deps) {
415
- const safeConfig = validConfig(config) ? config : DEFAULT_SPRING;
416
- if (safeConfig !== config) deps.warn?.("invalid spring configuration; using defaults");
417
- let state = { value: 0, velocity: 0 };
418
- let target = 0;
419
- let handle = null;
420
- let lastTime = 0;
421
- let startedAt = 0;
422
- function frame(time) {
423
- handle = null;
424
- const dt = (time - lastTime) / 1e3;
425
- lastTime = time;
426
- state = stepSpring(state, target, safeConfig, dt);
427
- if (!finiteState(state) || !Number.isFinite(target)) {
428
- abortRun("spring produced non-finite state");
429
- return;
430
- }
431
- if (time - startedAt >= MAX_SETTLE_MS) {
432
- abortRun(`spring exceeded ${MAX_SETTLE_MS}ms settle budget`);
433
- return;
434
- }
435
- if (isSettled(state, target, safeConfig)) {
436
- state = { value: target, velocity: 0 };
437
- onChange(state.value, true);
438
- return;
439
- }
440
- onChange(state.value, false);
441
- schedule();
442
- }
443
- function schedule() {
444
- if (handle !== null) return;
445
- handle = deps.requestFrame(frame);
446
- }
447
- function start() {
448
- lastTime = deps.now();
449
- startedAt = lastTime;
450
- schedule();
451
- }
452
- function abortRun(message) {
453
- state = { value: Number.isFinite(target) ? target : 0, velocity: 0 };
454
- deps.warn?.(message);
455
- onChange(state.value, true);
456
- }
457
- return {
458
- to(next, velocity) {
459
- target = next;
460
- if (velocity !== void 0) state = { ...state, velocity };
461
- start();
462
- },
463
- set(value, velocity = 0) {
464
- state = { value, velocity };
465
- },
466
- current: () => ({ ...state }),
467
- stop() {
468
- if (handle !== null) deps.cancelFrame(handle);
469
- handle = null;
470
- }
471
- };
472
- }
473
- function validConfig(config) {
474
- return Object.values(config).every(Number.isFinite) && config.stiffness > 0 && config.damping > 0 && config.mass > 0 && config.restVelocity > 0 && config.restDisplacement > 0;
475
- }
476
- function finiteState(state) {
477
- return Number.isFinite(state.value) && Number.isFinite(state.velocity);
478
- }
479
- function defaultSpringDeps() {
480
- const raf = globalThis.requestAnimationFrame;
481
- if (typeof raf !== "function") {
482
- return {
483
- requestFrame: (callback) => globalThis.setTimeout(() => callback(Date.now()), 16),
484
- cancelFrame: (handle) => globalThis.clearTimeout(handle),
485
- now: () => Date.now()
486
- };
487
- }
488
- return {
489
- requestFrame: (callback) => raf(callback),
490
- cancelFrame: (handle) => globalThis.cancelAnimationFrame(handle),
491
- now: () => performance.now()
492
- };
493
- }
494
-
495
- // src/effects/catalog/interaction-shared.ts
496
- function centeredOffset(point, size) {
497
- return {
498
- x: size.width > 0 ? point.x / size.width - 0.5 : 0,
499
- y: size.height > 0 ? point.y / size.height - 0.5 : 0
500
- };
501
- }
502
- function tiltAngles(point, size, maxAngleDeg) {
503
- const centered = centeredOffset(point, size);
504
- return { rotateY: centered.x * maxAngleDeg * 2, rotateX: -centered.y * maxAngleDeg * 2 };
505
- }
506
- function parallaxOffset(point, size, strengthPx) {
507
- const centered = centeredOffset(point, size);
508
- return { x: centered.x * strengthPx * 2, y: centered.y * strengthPx * 2 };
509
- }
510
- function supportsFineHover(win) {
511
- return win.matchMedia?.("(hover: hover) and (pointer: fine)").matches ?? true;
512
- }
513
-
514
- // src/effects/catalog/interaction.ts
515
- var hoverTiming = {
516
- duration: { type: "time", default: "220ms", cssProperty: "--kui-duration" },
517
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
518
- };
519
- var liftParams = {
520
- distance: { type: "length", default: "6px", cssProperty: "--kui-lift-distance" }
521
- };
522
- var popParams = {
523
- scale: { type: "number", default: "1.06", cssProperty: "--kui-pop-scale", finite: true, minimum: 0 }
524
- };
525
- var beamParams = {
526
- color: { type: "color", default: "", cssProperty: "--kui-beam-border-c1" },
527
- outset: { type: "length", default: "", cssProperty: "--kui-beam-border-outset" }
528
- };
529
- function hoverPrimitive(id, channels, extraParams = {}) {
530
- return {
531
- id,
532
- renderer: "javascript",
533
- channels,
534
- parameters: { ...hoverTiming, ...extraParams },
535
- supportedTimelines: ["time"],
536
- supportedActivations: ["load"],
537
- defaultActivation: "load",
538
- perfClass: "compositor",
539
- // Not 'disable': a translate/box-shadow/rotate hover micro-interaction at this scale is not a
540
- // vestibular trigger the way parallax or continuous ambient motion is. The real motion lives in
541
- // CSS transitions and `:hover`-scoped pseudo-element animations rather than this primitive's
542
- // (here unused) compiled `animation-*` path, so the policy layer shortens it via the
543
- // `transition-duration` and `::before`/`::after` rules in base.css.
544
- reducedMotion: "shorten",
545
- prepare: () => inertInstance()
546
- };
547
- }
548
- var HOVER_PRIMITIVES = [
549
- hoverPrimitive("lift", ["translate"], liftParams),
550
- hoverPrimitive("pop", ["scale"], popParams),
551
- hoverPrimitive("lift-shadow", ["translate", "shadow"], liftParams),
552
- hoverPrimitive("shine-sweep", ["sweep"]),
553
- hoverPrimitive("split-flap", ["rotate"]),
554
- hoverPrimitive("border-draw", ["border"]),
555
- hoverPrimitive("border-glow", ["shadow"]),
556
- hoverPrimitive("beam-border", ["border"], beamParams),
557
- hoverPrimitive("underline-slide", ["scale"]),
558
- hoverPrimitive("underline-center", ["scale"]),
559
- hoverPrimitive("icon-wiggle", ["rotate"]),
560
- hoverPrimitive("icon-spin", ["rotate"]),
561
- hoverPrimitive("icon-bounce", ["translate"])
562
- ];
563
- var HOVER_PRESETS = HOVER_PRIMITIVES.map((primitive) => ({
564
- name: primitive.id,
565
- primitive: primitive.id
566
- }));
567
- var CONTINUOUS_BORDER_PRIMITIVES = [
568
- {
569
- id: "beam-border-auto",
570
- renderer: "javascript",
571
- channels: ["border"],
572
- parameters: { ...hoverTiming, ...beamParams },
573
- supportedTimelines: ["time"],
574
- supportedActivations: ["load"],
575
- defaultActivation: "load",
576
- perfClass: "compositor",
577
- reducedMotion: "disable",
578
- prepare: () => inertInstance()
579
- }
580
- ];
581
- var CONTINUOUS_BORDER_PRESETS = [
582
- { name: "beam-border-auto", primitive: "beam-border-auto" }
583
- ];
584
- var springParams = {
585
- stiffness: {
586
- type: "number",
587
- default: "260",
588
- cssProperty: "--kui-stiffness",
589
- finite: true,
590
- minimum: 1,
591
- maximum: 1e4
592
- },
593
- damping: {
594
- type: "number",
595
- default: "26",
596
- cssProperty: "--kui-damping",
597
- finite: true,
598
- minimum: 0.1,
599
- maximum: 1e3
600
- },
601
- mass: {
602
- type: "number",
603
- default: "1",
604
- cssProperty: "--kui-mass",
605
- finite: true,
606
- minimum: 0.1
607
- }
608
- };
609
- function pointerPrimitive(id, channels, parameters, prepare) {
610
- return {
611
- id,
612
- renderer: "javascript",
613
- channels,
614
- parameters,
615
- supportedTimelines: ["time", "pointer"],
616
- supportedActivations: ["load", "manual"],
617
- defaultActivation: "load",
618
- perfClass: "continuous",
619
- // A tilt or cursor-follow effect only exists while the pointer is present; there is no
620
- // meaningful "shortened" version of tracking a position. `prepare` itself checks
621
- // `supportsFineHover` and no-ops on touch, which is the coarse-pointer half of this rule.
622
- reducedMotion: "disable",
623
- prepare
624
- };
625
- }
626
- function springFrom(params) {
627
- return {
628
- ...DEFAULT_SPRING,
629
- stiffness: params.num("stiffness", DEFAULT_SPRING.stiffness),
630
- damping: params.num("damping", DEFAULT_SPRING.damping),
631
- mass: params.num("mass", DEFAULT_SPRING.mass)
632
- };
633
- }
634
- function springDepsFor(ctx) {
635
- return { ...defaultSpringDeps(), warn: ctx.warn };
636
- }
637
- function prepareTilt3d(el, params, ctx) {
638
- if (!supportsFineHover(ctx.win)) return () => {
639
- };
640
- const node = el;
641
- const maxAngle = params.num("maxAngle", 14);
642
- const perspective = params.text("perspective", "800px");
643
- function writeAngles(rotateX, rotateY, transitionMs) {
644
- ctx.style.set("transition", `transform ${transitionMs}ms ease-out`);
645
- ctx.style.set(
646
- "transform",
647
- `perspective(${perspective}) rotateX(${rotateX.toFixed(2)}deg) rotateY(${rotateY.toFixed(2)}deg)`
648
- );
649
- }
650
- function onMove(event) {
651
- const rect = node.getBoundingClientRect();
652
- const point = { x: event.clientX - rect.left, y: event.clientY - rect.top };
653
- const { rotateX, rotateY } = tiltAngles(point, rect, maxAngle);
654
- writeAngles(rotateX, rotateY, 100);
655
- }
656
- function reset() {
657
- writeAngles(0, 0, 400);
658
- }
659
- function onFocus() {
660
- writeAngles(-maxAngle / 2, maxAngle / 2, 250);
661
- }
662
- node.addEventListener("pointermove", onMove, { passive: true });
663
- node.addEventListener("pointerleave", reset, { passive: true });
664
- node.addEventListener("focus", onFocus);
665
- node.addEventListener("blur", reset);
666
- return () => {
667
- node.removeEventListener("pointermove", onMove);
668
- node.removeEventListener("pointerleave", reset);
669
- node.removeEventListener("focus", onFocus);
670
- node.removeEventListener("blur", reset);
671
- };
672
- }
673
- function collectParallaxLayers(node) {
674
- return [...node.querySelectorAll("[data-depth]")].map((layer) => ({
675
- ledger: createStyleLedger(layer),
676
- depth: Number(layer.dataset.depth) || 0
677
- }));
678
- }
679
- function writeParallaxLayers(layers, x, y, transitionMs) {
680
- for (const layer of layers) {
681
- layer.ledger.set("transition", `translate ${transitionMs}ms ease-out`);
682
- layer.ledger.set("translate", `${(x * layer.depth).toFixed(2)}px ${(y * layer.depth).toFixed(2)}px`);
683
- }
684
- }
685
- function prepareTiltParallax(el, params, ctx) {
686
- if (!supportsFineHover(ctx.win)) return () => {
687
- };
688
- const node = el;
689
- const strength = params.num("strength", 24);
690
- const layers = collectParallaxLayers(node);
691
- function onMove(event) {
692
- const rect = node.getBoundingClientRect();
693
- const point = { x: event.clientX - rect.left, y: event.clientY - rect.top };
694
- const offset = parallaxOffset(point, rect, strength);
695
- writeParallaxLayers(layers, offset.x, offset.y, 120);
696
- }
697
- function reset() {
698
- writeParallaxLayers(layers, 0, 0, 400);
699
- }
700
- node.addEventListener("pointermove", onMove, { passive: true });
701
- node.addEventListener("pointerleave", reset, { passive: true });
702
- node.addEventListener("focus", reset);
703
- node.addEventListener("blur", reset);
704
- return () => {
705
- node.removeEventListener("pointermove", onMove);
706
- node.removeEventListener("pointerleave", reset);
707
- node.removeEventListener("focus", reset);
708
- node.removeEventListener("blur", reset);
709
- for (const layer of layers) layer.ledger.restore();
710
- };
711
- }
712
- function writeDotPosition(dot, x, y) {
713
- dot.style.translate = `${x.toFixed(1)}px ${y.toFixed(1)}px`;
714
- }
715
- function createCursorDot(doc, dotClass, labelText) {
716
- const dot = doc.createElement("span");
717
- dot.className = `kui-cursor-dot ${dotClass}`;
718
- dot.setAttribute("aria-hidden", "true");
719
- if (labelText) dot.textContent = labelText;
720
- doc.body.append(dot);
721
- return dot;
722
- }
723
- function createCursorRunners(dot, config, deps) {
724
- const position = { x: 0, y: 0 };
725
- const onAxis = (axis) => (value) => {
726
- position[axis] = value;
727
- writeDotPosition(dot, position.x, position.y);
728
- };
729
- return {
730
- x: createSpringRunner(config, onAxis("x"), deps),
731
- y: createSpringRunner(config, onAxis("y"), deps)
732
- };
733
- }
734
- function prepareCursorDot(el, params, ctx, dotClass) {
735
- if (!supportsFineHover(ctx.win)) return () => {
736
- };
737
- const doc = el.ownerDocument;
738
- const node = el;
739
- const dot = createCursorDot(doc, dotClass, params.text("label", ""));
740
- const runners = createCursorRunners(dot, springFrom(params), springDepsFor(ctx));
741
- function show() {
742
- dot.classList.add("kui-cursor-dot-active");
743
- }
744
- function hide() {
745
- dot.classList.remove("kui-cursor-dot-active");
746
- }
747
- function onMove(event) {
748
- show();
749
- runners.x.to(event.clientX);
750
- runners.y.to(event.clientY);
751
- }
752
- function onFocus() {
753
- const rect = node.getBoundingClientRect();
754
- const cx = rect.left + rect.width / 2;
755
- const cy = rect.top + rect.height / 2;
756
- show();
757
- runners.x.set(cx);
758
- runners.y.set(cy);
759
- writeDotPosition(dot, cx, cy);
760
- }
761
- node.addEventListener("pointermove", onMove, { passive: true });
762
- node.addEventListener("pointerleave", hide, { passive: true });
763
- node.addEventListener("focus", onFocus);
764
- node.addEventListener("blur", hide);
765
- return () => {
766
- node.removeEventListener("pointermove", onMove);
767
- node.removeEventListener("pointerleave", hide);
768
- node.removeEventListener("focus", onFocus);
769
- node.removeEventListener("blur", hide);
770
- runners.x.stop();
771
- runners.y.stop();
772
- dot.remove();
773
- };
774
- }
775
- function prepareSpotlight(el, params, ctx) {
776
- if (!supportsFineHover(ctx.win)) return () => {
777
- };
778
- const node = el;
779
- if (ctx.win.getComputedStyle(node).position === "static") ctx.style.set("position", "relative");
780
- function writeSpot(x, y, on) {
781
- ctx.style.set("--kui-x", x);
782
- ctx.style.set("--kui-y", y);
783
- ctx.style.set("--kui-spotlight-opacity", on ? "1" : "0");
784
- }
785
- function onMove(event) {
786
- const rect = node.getBoundingClientRect();
787
- writeSpot(`${(event.clientX - rect.left).toFixed(1)}px`, `${(event.clientY - rect.top).toFixed(1)}px`, true);
788
- }
789
- function hide() {
790
- writeSpot("50%", "50%", false);
791
- }
792
- function onFocus() {
793
- writeSpot("50%", "50%", true);
794
- }
795
- node.addEventListener("pointermove", onMove, { passive: true });
796
- node.addEventListener("pointerleave", hide, { passive: true });
797
- node.addEventListener("focus", onFocus);
798
- node.addEventListener("blur", hide);
799
- return () => {
800
- node.removeEventListener("pointermove", onMove);
801
- node.removeEventListener("pointerleave", hide);
802
- node.removeEventListener("focus", onFocus);
803
- node.removeEventListener("blur", hide);
804
- };
805
- }
806
- var tiltParams = {
807
- maxAngle: { type: "number", default: "14", cssProperty: "--kui-max-angle" },
808
- perspective: { type: "length", default: "800px", cssProperty: "--kui-perspective" }
809
- };
810
- var parallaxParams = {
811
- strength: { type: "number", default: "24", cssProperty: "--kui-strength" }
812
- };
813
- var cursorDotParams = {
814
- ...springParams,
815
- label: { type: "text", default: "", cssProperty: "--kui-label" }
816
- };
817
- var POINTER_PRIMITIVES = [
818
- pointerPrimitive("tilt-3d", ["rotate"], tiltParams, deferPrepare(prepareTilt3d)),
819
- pointerPrimitive("tilt-parallax", ["translate"], parallaxParams, deferPrepare(prepareTiltParallax)),
820
- pointerPrimitive("cursor-follow", ["translate"], cursorDotParams, deferPrepare(prepareCursorFollow)),
821
- pointerPrimitive("cursor-lag", ["translate"], cursorDotParams, deferPrepare(prepareCursorLag)),
822
- pointerPrimitive("cursor-label", ["translate"], cursorDotParams, deferPrepare(prepareCursorLabel)),
823
- pointerPrimitive("cursor-invert", ["translate"], cursorDotParams, deferPrepare(prepareCursorInvert)),
824
- pointerPrimitive("cursor-spotlight", ["spotlight"], {}, deferPrepare(prepareSpotlight))
825
- ];
826
- function prepareCursorFollow(el, params, ctx) {
827
- return prepareCursorDot(el, params, ctx, "kui-cursor-dot-follow");
828
- }
829
- function prepareCursorLag(el, params, ctx) {
830
- return prepareCursorDot(el, params, ctx, "kui-cursor-dot-lag");
831
- }
832
- function prepareCursorLabel(el, params, ctx) {
833
- return prepareCursorDot(el, params, ctx, "kui-cursor-dot-label");
834
- }
835
- function prepareCursorInvert(el, params, ctx) {
836
- return prepareCursorDot(el, params, ctx, "kui-cursor-dot-invert");
837
- }
838
- var POINTER_PRESETS = [
839
- { name: "tilt-3d", primitive: "tilt-3d" },
840
- { name: "tilt-parallax", primitive: "tilt-parallax" },
841
- { name: "cursor-follow", primitive: "cursor-follow", params: { stiffness: "300", damping: "30" } },
842
- { name: "cursor-lag", primitive: "cursor-lag", params: { stiffness: "80", damping: "14" } },
843
- { name: "cursor-label", primitive: "cursor-label", params: { stiffness: "260", damping: "26", label: "View" } },
844
- { name: "cursor-spotlight", primitive: "cursor-spotlight" },
845
- { name: "cursor-invert", primitive: "cursor-invert", params: { stiffness: "260", damping: "26" } }
846
- ];
847
- var INTERACTION_PRIMITIVES = [
848
- ...HOVER_PRIMITIVES,
849
- ...POINTER_PRIMITIVES,
850
- ...CONTINUOUS_BORDER_PRIMITIVES
851
- ];
852
- var INTERACTION_PRESETS = [
853
- ...HOVER_PRESETS,
854
- ...POINTER_PRESETS,
855
- ...CONTINUOUS_BORDER_PRESETS
856
- ];
857
- function registerInteraction(registry) {
858
- return registry.registerPrimitives(INTERACTION_PRIMITIVES).registerPresets(INTERACTION_PRESETS);
859
- }
860
-
861
- // src/effects/catalog/background-media.ts
862
- var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "webm", "mov", "m4v", "ogv"]);
863
- var VIDEO_PAGE_HOSTS = /* @__PURE__ */ new Set([
864
- "youtube.com",
865
- "www.youtube.com",
866
- "m.youtube.com",
867
- "youtube-nocookie.com",
868
- "www.youtube-nocookie.com",
869
- "youtu.be",
870
- "www.youtu.be"
871
- ]);
872
- function normalizeUrl(value) {
873
- const stripped = value.replace(/[\t\n\r]/g, "");
874
- let start = 0;
875
- while (start < stripped.length && stripped.charCodeAt(start) <= 32) start += 1;
876
- return stripped.slice(start);
877
- }
878
- function hostOf(value) {
879
- const withoutScheme = normalizeUrl(value).replace(/^[a-z][a-z0-9+.-]*:/i, "").replace(/^\/\//, "");
880
- const end = withoutScheme.search(/[/?#]/);
881
- const authority = end === -1 ? withoutScheme : withoutScheme.slice(0, end);
882
- return authority.slice(authority.lastIndexOf("@") + 1).toLowerCase();
883
- }
884
- function isVideoPageUrl(value) {
885
- return VIDEO_PAGE_HOSTS.has(hostOf(value));
886
- }
887
- var FOCAL_POINTS = {
888
- center: "50% 50%",
889
- top: "50% 0%",
890
- bottom: "50% 100%",
891
- left: "0% 50%",
892
- right: "100% 50%",
893
- "top-left": "0% 0%",
894
- "top-right": "100% 0%",
895
- "bottom-left": "0% 100%",
896
- "bottom-right": "100% 100%"
897
- };
898
- var FOCAL_POINT_NAMES = Object.keys(FOCAL_POINTS);
899
- function focalPosition(focus) {
900
- return FOCAL_POINTS[focus] ?? FOCAL_POINTS.center;
901
- }
902
- function paintsOverlay(options) {
903
- return options.overlay !== "transparent" && options.overlayOpacity > 0;
904
- }
905
- function isVideoSource(src) {
906
- const path = src.split("?")[0].split("#")[0];
907
- const dot = path.lastIndexOf(".");
908
- if (dot <= path.lastIndexOf("/")) return false;
909
- return VIDEO_EXTENSIONS.has(path.slice(dot + 1).toLowerCase());
910
- }
911
- var ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http", "https"]);
912
- function schemeOf(value) {
913
- const match = /^([a-z][a-z0-9+.-]*):/i.exec(normalizeUrl(value));
914
- return match ? match[1].toLowerCase() : "";
915
- }
916
- function mediaSource(authored, name, ctx) {
917
- if (!authored) return authored;
918
- if (isVideoPageUrl(authored)) {
919
- ctx.warn(
920
- `background-media "${name}": "${authored}" is a video *page*, not a media file \u2014 a YouTube URL cannot be played by <video> and needs an iframe embed instead. Point "${name}" at an .mp4/.webm file, or use a YouTube facade alongside this effect.`
921
- );
922
- return "";
923
- }
924
- const scheme = schemeOf(authored);
925
- if (!scheme || ALLOWED_SCHEMES.has(scheme)) return authored;
926
- ctx.warn(
927
- `background-media "${name}": "${scheme}:" URLs are not allowed \u2014 use https:, http:, or a path such as "/media/hero.mp4".`
928
- );
929
- return "";
930
- }
931
- function styleLayer(node) {
932
- const { style } = node;
933
- style.setProperty("position", "absolute");
934
- style.setProperty("top", "0");
935
- style.setProperty("left", "0");
936
- style.setProperty("width", "100%");
937
- style.setProperty("height", "100%");
938
- style.setProperty("border-radius", "inherit");
939
- style.setProperty("pointer-events", "none");
940
- style.setProperty("z-index", "-1");
941
- }
942
- function createOverlay(doc, options) {
943
- const scrim = doc.createElement("div");
944
- styleLayer(scrim);
945
- scrim.style.setProperty("background", options.overlay);
946
- scrim.style.setProperty("opacity", String(options.overlayOpacity));
947
- scrim.setAttribute("aria-hidden", "true");
948
- return scrim;
949
- }
950
- function createVideo(doc, options) {
951
- const video = doc.createElement("video");
952
- video.muted = true;
953
- video.setAttribute("muted", "");
954
- video.playsInline = true;
955
- video.setAttribute("playsinline", "");
956
- video.loop = options.loop;
957
- video.defaultPlaybackRate = options.rate;
958
- video.playbackRate = options.rate;
959
- video.preload = "metadata";
960
- if (options.poster) video.poster = options.poster;
961
- video.src = options.src;
962
- return video;
963
- }
964
- function play(video) {
965
- const started = video.play();
966
- if (started) void started.catch(() => {
967
- });
968
- }
969
- function startPlayback(video, win, options) {
970
- if (options.reducedMotion || options.autoplay === "never") return () => {
971
- };
972
- if (options.autoplay === "always") {
973
- play(video);
974
- return () => {
975
- if (!video.paused) video.pause();
976
- };
977
- }
978
- return autoplayInView(video, win);
979
- }
980
- function createImage(doc, options) {
981
- const image = doc.createElement("img");
982
- image.alt = "";
983
- image.decoding = "async";
984
- image.src = options.src;
985
- return image;
986
- }
987
- function autoplayInView(video, win) {
988
- const Observer = win.IntersectionObserver;
989
- if (!Observer) return () => {
990
- };
991
- const observer = new Observer(
992
- (entries) => {
993
- for (const entry of entries) {
994
- if (entry.isIntersecting) play(video);
995
- else if (!video.paused) video.pause();
996
- }
997
- },
998
- { threshold: 0 }
999
- );
1000
- observer.observe(video);
1001
- return () => {
1002
- observer.disconnect();
1003
- if (!video.paused) video.pause();
1004
- };
1005
- }
1006
- function installBackgroundMedia(el, ctx, options) {
1007
- const doc = el.ownerDocument;
1008
- const video = isVideoSource(options.src) ? createVideo(doc, options) : null;
1009
- const node = video ?? createImage(doc, options);
1010
- styleLayer(node);
1011
- node.style.setProperty("object-fit", options.fit);
1012
- node.style.setProperty("object-position", options.position);
1013
- node.setAttribute("aria-hidden", "true");
1014
- node.setAttribute("data-kui-background", "");
1015
- el.append(node);
1016
- const overlay = paintsOverlay(options) ? createOverlay(doc, options) : null;
1017
- if (overlay) {
1018
- overlay.setAttribute("data-kui-background-overlay", "");
1019
- el.append(overlay);
1020
- }
1021
- const stopPlayback = video ? startPlayback(video, ctx.win, options) : () => {
1022
- };
1023
- return {
1024
- remove: () => {
1025
- stopPlayback();
1026
- node.remove();
1027
- overlay?.remove();
1028
- }
1029
- };
1030
- }
1031
-
1032
- // src/effects/catalog/media-shared.ts
1033
- var AXIS_DEGREES = { vertical: 0, horizontal: 90 };
1034
- var UNIT_DEGREES = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
1035
- var BAND_OVERLAP_PX = 1;
1036
- var STAGE_CLASS = "kui-slat-stage";
1037
- var SLAT_CLASS = "kui-slat-item";
1038
- var GOLDEN_RATIO_CONJUGATE = 0.6180339887498949;
1039
- function slatOrder(index, count, from) {
1040
- if (count <= 1) return 0;
1041
- switch (from) {
1042
- case "start":
1043
- return index;
1044
- case "end":
1045
- return count - 1 - index;
1046
- case "edges":
1047
- return Math.min(index, count - 1 - index);
1048
- case "random-ish":
1049
- return Math.floor(index * GOLDEN_RATIO_CONJUGATE % 1 * count);
1050
- case "alternate":
1051
- default:
1052
- return zigzagRank(index, count);
1053
- }
1054
- }
1055
- function zigzagRank(index, count) {
1056
- const fromStart = index;
1057
- const fromEnd = count - 1 - index;
1058
- const pair = Math.min(fromStart, fromEnd);
1059
- return fromStart <= fromEnd ? pair * 2 : pair * 2 + 1;
1060
- }
1061
- function slatAngleDegrees(authored, axis) {
1062
- const trimmed = authored.trim();
1063
- if (!trimmed) return AXIS_DEGREES[axis];
1064
- const match = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(deg|rad|grad|turn)?$/.exec(trimmed);
1065
- if (!match) return AXIS_DEGREES[axis];
1066
- const value = Number(match[1]);
1067
- if (!Number.isFinite(value)) return AXIS_DEGREES[axis];
1068
- const unit = match[2] ?? "deg";
1069
- const degrees = value * UNIT_DEGREES[unit];
1070
- return (degrees % 180 + 180) % 180;
1071
- }
1072
- function slatTravelVector(angleDegrees) {
1073
- const theta = angleDegrees * Math.PI / 180;
1074
- return { x: -Math.sin(theta), y: Math.cos(theta) };
1075
- }
1076
- function slatBandClip(index, count, angleDegrees, box) {
1077
- const { width, height } = box;
1078
- const theta = angleDegrees * Math.PI / 180;
1079
- const nx = Math.cos(theta);
1080
- const ny = Math.sin(theta);
1081
- const { x: dx, y: dy } = slatTravelVector(angleDegrees);
1082
- const span = Math.abs(nx) * width + Math.abs(ny) * height;
1083
- const centreX = width / 2;
1084
- const centreY = height / 2;
1085
- const step = span / count;
1086
- const near = index * step - span / 2 - BAND_OVERLAP_PX;
1087
- const far = (index + 1) * step - span / 2 + BAND_OVERLAP_PX;
1088
- const reach = width + height;
1089
- const corner = (along, across) => `${(centreX + along * nx + across * dx).toFixed(2)}px ${(centreY + along * ny + across * dy).toFixed(2)}px`;
1090
- return `polygon(${corner(near, reach)}, ${corner(near, -reach)}, ${corner(far, -reach)}, ${corner(far, reach)})`;
1091
- }
1092
- function axisLabel(angleDegrees) {
1093
- if (angleDegrees === 0) return "vertical";
1094
- if (angleDegrees === 90) return "horizontal";
1095
- return "diagonal";
1096
- }
1097
- function syncStageToImage(stage, img, bands) {
1098
- const { slats, angleDegrees } = bands;
1099
- const width = img.offsetWidth;
1100
- const height = img.offsetHeight;
1101
- stage.style.top = `${img.offsetTop}px`;
1102
- stage.style.left = `${img.offsetLeft}px`;
1103
- stage.style.width = `${width}px`;
1104
- stage.style.height = `${height}px`;
1105
- slats.forEach((slat, index) => {
1106
- slat.style.clipPath = slatBandClip(index, slats.length, angleDegrees, { width, height });
1107
- });
1108
- }
1109
- function watchImageBox(stage, img, win, bands) {
1110
- const handler = () => syncStageToImage(stage, img, bands);
1111
- win.addEventListener("resize", handler, { passive: true });
1112
- const stopWindow = () => win.removeEventListener("resize", handler);
1113
- const ResizeObserverCtor = win.ResizeObserver;
1114
- if (!ResizeObserverCtor) return stopWindow;
1115
- const observer = new ResizeObserverCtor(handler);
1116
- observer.observe(img);
1117
- return () => {
1118
- stopWindow();
1119
- observer.disconnect();
1120
- };
1121
- }
1122
- function imagePaintStyle(img, win) {
1123
- const computed = win.getComputedStyle(img);
1124
- const position = computed.objectPosition || "50% 50%";
1125
- const fit = computed.objectFit;
1126
- if (fit === "cover") return { size: "cover", position };
1127
- if (fit === "contain" || fit === "scale-down") return { size: "contain", position };
1128
- if (fit === "none") return { size: "auto", position };
1129
- return { size: "100% 100%", position };
1130
- }
1131
- function installSlatStage(el, doc, win, options) {
1132
- const img = el.querySelector("img");
1133
- if (!img) return null;
1134
- const url = img.currentSrc || img.getAttribute("src") || "";
1135
- if (!url) return null;
1136
- const { count, angleDegrees, from, fold } = options;
1137
- const travel = slatTravelVector(angleDegrees);
1138
- const stage = doc.createElement("div");
1139
- stage.className = STAGE_CLASS;
1140
- stage.setAttribute("aria-hidden", "true");
1141
- stage.dataset.kuiSlatAxis = axisLabel(angleDegrees);
1142
- stage.dataset.kuiSlatAngle = String(angleDegrees);
1143
- stage.dataset.kuiSlatFold = String(fold);
1144
- stage.style.setProperty("--kui-slat-count", String(count));
1145
- stage.style.setProperty("--kui-slat-dx", travel.x.toFixed(4));
1146
- stage.style.setProperty("--kui-slat-dy", travel.y.toFixed(4));
1147
- const paint = imagePaintStyle(img, win);
1148
- const slats = [];
1149
- for (let index = 0; index < count; index++) {
1150
- const slat = doc.createElement("div");
1151
- slat.className = SLAT_CLASS;
1152
- slat.style.setProperty("--kui-slat-index", String(index));
1153
- slat.style.setProperty("--kui-i", String(slatOrder(index, count, from)));
1154
- slat.style.backgroundImage = `url("${url}")`;
1155
- slat.style.backgroundSize = paint.size;
1156
- slat.style.backgroundPosition = paint.position;
1157
- stage.append(slat);
1158
- slats.push(slat);
1159
- }
1160
- el.append(stage);
1161
- const bands = { slats, angleDegrees };
1162
- syncStageToImage(stage, img, bands);
1163
- const stopWatching = watchImageBox(stage, img, win, bands);
1164
- const imageStyles = createStyleLedger(img);
1165
- imageStyles.set("visibility", "hidden");
1166
- return {
1167
- stage,
1168
- slats,
1169
- restore: () => {
1170
- stopWatching();
1171
- imageStyles.restore();
1172
- stage.remove();
1173
- }
1174
- };
1175
- }
1176
- function applySlatTimingVars(stage, params) {
1177
- const { durationMs, delayMs, easing } = params.timing;
1178
- const duration = durationMs === void 0 ? params.text("duration", "500ms") : `${durationMs}ms`;
1179
- const delay = delayMs === void 0 ? params.text("delay", "0ms") : `${delayMs}ms`;
1180
- stage.style.setProperty("--kui-duration", duration);
1181
- stage.style.setProperty("--kui-delay", delay);
1182
- stage.style.setProperty("--kui-ease", easing ?? params.text("ease", "ease-out"));
1183
- stage.style.setProperty("--kui-stagger", params.text("stagger", "60ms"));
1184
- }
1185
- function slatAssembleFinishMs(params, count) {
1186
- if (count === 0) return 0;
1187
- const durationMs = params.timing.durationMs ?? params.ms("duration", 500);
1188
- const delayMs = params.timing.delayMs ?? params.ms("delay", 0);
1189
- const staggerMs = params.ms("stagger", 60);
1190
- return delayMs + (count - 1) * staggerMs + durationMs;
1191
- }
1192
-
1193
- // src/effects/catalog/media.ts
1194
- var geometry = {
1195
- distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
1196
- scale: { type: "number", default: "1.12", cssProperty: "--kui-to-scale" }
1197
- };
1198
- var MEDIA_CSS_PRIMITIVES = [
1199
- cssPrimitive("media-wipe", [CHANNEL.clip]),
1200
- cssPrimitive("media-mask", ["mask"], { perfClass: "paint" }),
1201
- // Not `reducedMotion: 'disable'` — that policy means "no finite duration would make sense,
1202
- // because the animation never ends" (see `ambient.ts`/`feedback.ts`), and a Ken Burns pan/zoom
1203
- // is the opposite of that: a one-shot cinematic move with a real, shortenable duration. The demo
1204
- // authors it as `ken-burns 9000ms` (a still image, one slow zoom, then it holds) and
1205
- // `ken-burns 3000ms on:hover` (zooms in while hovered), and its complement `ken-burns-out` is a
1206
- // second one-shot preset for the reverse move — not a `-loop` variant the way `typewriter-loop`
1207
- // or `marquee`/`marquee-scroll-linked` are. `kui-ken-burns`'s keyframe (`scale: 1` to `1.12`,
1208
- // no loop-safe midpoint) is shaped for exactly that: run once, land on the zoomed frame, stay
1209
- // there. `'disable'` here previously looked like the same missing-`--kui-fx-*-iterations` bug as
1210
- // `marquee`/`gradient-shimmer`, but the actual defect was this policy — the default `'shorten'`
1211
- // is correct, so no `--kui-fx-ken-burns-iterations` is needed at all.
1212
- cssPrimitive("media-ken-burns", [CHANNEL.translate, CHANNEL.scale], {
1213
- parameters: geometry
1214
- }),
1215
- cssPrimitive("media-filter", [CHANNEL.filter], {
1216
- defaultActivation: "hover",
1217
- perfClass: "paint"
1218
- }),
1219
- // `geometry.distance` only, not `...geometry`: `kui-blur-up` (media.css) reads `--kui-distance`
1220
- // and `--kui-blur` but never `--kui-to-scale` — this primitive doesn't even declare
1221
- // `CHANNEL.scale`. Spreading the whole shared object used to expose `scale:` as an
1222
- // apparently-valid, silently-inert parameter, the same shape `flip-3d`'s dead `perspective`
1223
- // parameter was (`entrance.css`'s comment on `kui-flip-in-x`).
1224
- cssPrimitive("media-blur-up", [CHANNEL.translate, CHANNEL.filter], {
1225
- parameters: {
1226
- distance: geometry.distance,
1227
- blur: { type: "length", default: "16px", cssProperty: "--kui-blur" }
1228
- },
1229
- perfClass: "paint"
1230
- }),
1231
- // No `defaultActivation` — same convention `core.ts`'s `parallax`/`parallax-scale`/
1232
- // `parallax-rotate`/`scroll-fade`/`desaturate`/`skew`/`progress`/`progress-stroke` already use
1233
- // for every other `timelines: ['view', 'scroll', ...]` primitive. `resolveActivation`
1234
- // (`animator.ts`) only consults `defaultActivation` when the author named no activation, and
1235
- // falls through to `element-config.ts`'s hardcoded `'enter'` when a primitive declares none.
1236
- // Setting it to `'manual'` here (matching `activations: ['manual']`) looked like the obviously
1237
- // correct pairing, but it is what actually broke the effect: `effectiveActivation`
1238
- // (`style-plan.ts`) only converts a stuck `'manual'` into `'enter'` when `config.timeline !==
1239
- // 'time'` — i.e. only once the author has actually written `timeline:view`/`timeline:scroll`.
1240
- // Authored bare (no `timeline:`, the sweep's own probe and the likely first thing anyone
1241
- // tries), `config.timeline` stays the default `'time'`, that conversion never fires, and the
1242
- // element sits at `data-kui-state="ready"` forever — which is also the state
1243
- // `entrance.css`'s `[data-kui-state='ready'] { --kui-distance: 0px !important; }` targets, so
1244
- // every sample read the same permanently-zeroed `--kui-distance`: not a paused animation, a
1245
- // zeroed one. `timeline:view`/`timeline:scroll` usage is unaffected either way, since a native
1246
- // timeline resolves to the `'native-timeline'` gate before activation is even consulted.
1247
- // `geometry.distance` only: `kui-image-parallax-frame` never reads `--kui-to-scale` and this
1248
- // primitive doesn't declare `CHANNEL.scale` — same dead-parameter shape as `media-blur-up` above.
1249
- cssPrimitive("media-parallax-frame", [CHANNEL.translate], {
1250
- parameters: { distance: geometry.distance },
1251
- timelines: ["view", "scroll"],
1252
- activations: ["manual"],
1253
- reducedMotion: "disable"
1254
- }),
1255
- // Its own `scale` parameter, not `geometry`: `kui-lightbox-open` opens from a fixed 0.92, never
1256
- // reading `--kui-to-scale` (or any `distance`, since this primitive doesn't animate position at
1257
- // all) — the whole shared object was dead weight here. Unlike `media-blur-up`/
1258
- // `media-parallax-frame`, this primitive *does* declare `CHANNEL.scale`, so the fix is to wire
1259
- // the keyframe up to a real parameter rather than remove the promise: `--kui-from-scale`, the
1260
- // same name and "starts at this scale, animates to 1" meaning `scale`/`scale-move`
1261
- // (`catalog/core.ts`) already use, so `lightbox-open scale:0.8` now does what it looks like it
1262
- // should. See `kui-lightbox-open` in `media.css`.
1263
- cssPrimitive("media-lightbox", [CHANNEL.opacity, CHANNEL.scale], {
1264
- parameters: { scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" } }
1265
- })
1266
- ];
1267
- var MEDIA_CSS_PRESETS = [
1268
- { name: "wipe-up", primitive: "media-wipe", keyframes: "kui-wipe-up", cloak: true },
1269
- { name: "wipe-down", primitive: "media-wipe", keyframes: "kui-wipe-down", cloak: true },
1270
- { name: "wipe-left", primitive: "media-wipe", keyframes: "kui-wipe-left", cloak: true },
1271
- { name: "wipe-right", primitive: "media-wipe", keyframes: "kui-wipe-right", cloak: true },
1272
- { name: "wipe-circle", primitive: "media-wipe", keyframes: "kui-wipe-circle", cloak: true },
1273
- { name: "wipe-diagonal", primitive: "media-wipe", keyframes: "kui-wipe-diagonal", cloak: true },
1274
- { name: "mask-reveal", primitive: "media-mask", keyframes: "kui-mask-reveal", cloak: true },
1275
- { name: "curtain-reveal", primitive: "media-wipe", keyframes: "kui-curtain-reveal", cloak: true },
1276
- { name: "ken-burns", primitive: "media-ken-burns", keyframes: "kui-ken-burns" },
1277
- { name: "ken-burns-out", primitive: "media-ken-burns", keyframes: "kui-ken-burns-out" },
1278
- { name: "blur-up", primitive: "media-blur-up", keyframes: "kui-blur-up", cloak: true },
1279
- { name: "duotone-hover", primitive: "media-filter", keyframes: "kui-duotone-hover" },
1280
- { name: "grayscale-hover", primitive: "media-filter", keyframes: "kui-grayscale-hover" },
1281
- { name: "saturate-hover", primitive: "media-filter", keyframes: "kui-saturate-hover" },
1282
- {
1283
- name: "image-parallax-frame",
1284
- primitive: "media-parallax-frame",
1285
- keyframes: "kui-image-parallax-frame"
1286
- },
1287
- { name: "before-after-wipe", primitive: "media-wipe", keyframes: "kui-before-after-wipe" },
1288
- { name: "lightbox-open", primitive: "media-lightbox", keyframes: "kui-lightbox-open" }
1289
- ];
1290
- var slatParams = {
1291
- slats: {
1292
- type: "number",
1293
- default: "8",
1294
- cssProperty: "--kui-slats",
1295
- minimum: 2,
1296
- maximum: 24,
1297
- integer: true
1298
- },
1299
- axis: {
1300
- type: "keyword",
1301
- default: "vertical",
1302
- cssProperty: "--kui-axis",
1303
- values: ["vertical", "horizontal"]
1304
- },
1305
- /*
1306
- * The general form of `axis:`, in degrees: `0deg` is `axis:vertical`, `90deg` is
1307
- * `axis:horizontal`, and anything between cuts the picture into diagonal bands. An authored
1308
- * angle wins; leaving it off reads `axis:`, so every existing attribute keeps its meaning.
1309
- *
1310
- * `text`, not `angle`, for one reason: `readParams` pre-fills every declared parameter with its
1311
- * schema default, so a typed default is indistinguishable from an authored value and there would
1312
- * be no way to tell `angle:0deg` from "no angle, use the axis". An empty default is only possible
1313
- * on a type that is never written to a stylesheet, which is exactly what `text` is for — and this
1314
- * value never reaches CSS anyway. `slatAngleDegrees` does the parsing and the range clamp.
1315
- */
1316
- angle: { type: "text", default: "", cssProperty: "--kui-slat-angle" },
1317
- from: {
1318
- type: "keyword",
1319
- default: "alternate",
1320
- cssProperty: "--kui-from",
1321
- values: ["alternate", "start", "end", "edges", "random-ish"]
1322
- },
1323
- fold: { type: "keyword", default: "false", cssProperty: "--kui-fold", values: ["true", "false"] },
1324
- duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
1325
- ...TRIGGER_DELAY_PARAM,
1326
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
1327
- stagger: { type: "time", default: "60ms", cssProperty: "--kui-stagger" }
1328
- };
1329
- function prepareSlatAssemble(el, params, ctx) {
1330
- const doc = el.ownerDocument;
1331
- const count = Math.min(24, Math.max(2, Math.round(params.num("slats", 8))));
1332
- const axis = params.text("axis", "vertical");
1333
- const angleDegrees = slatAngleDegrees(params.text("angle", ""), axis);
1334
- const from = params.text("from", "alternate");
1335
- const fold = params.is("fold");
1336
- const node = el;
1337
- if (ctx.win.getComputedStyle(node).position === "static") ctx.style.set("position", "relative");
1338
- const built = installSlatStage(el, doc, ctx.win, { count, angleDegrees, from, fold });
1339
- if (!built) return () => {
1340
- };
1341
- const { stage } = built;
1342
- applySlatTimingVars(stage, params);
1343
- stage.classList.add("kui-slat-animating");
1344
- let settle2;
1345
- const finished = new Promise((resolve) => {
1346
- settle2 = resolve;
1347
- });
1348
- let landed = false;
1349
- const land = () => {
1350
- if (landed) return;
1351
- landed = true;
1352
- stage.classList.remove("kui-slat-animating");
1353
- built.restore();
1354
- settle2();
1355
- };
1356
- const timer = ctx.win.setTimeout(land, slatAssembleFinishMs(params, count));
1357
- return {
1358
- cleanup: () => {
1359
- ctx.win.clearTimeout(timer);
1360
- if (landed) return;
1361
- landed = true;
1362
- built.restore();
1363
- },
1364
- finished,
1365
- finish: () => {
1366
- ctx.win.clearTimeout(timer);
1367
- land();
1368
- }
1369
- };
1370
- }
1371
- var backgroundMediaParams = {
1372
- /*
1373
- * `text`, and same-origin-checked at the point of use rather than by the type — identical
1374
- * shape and identical reasoning to `media-scrub`'s own `src` (`scroll-mechanics/primitives.ts`).
1375
- * A URL has no lexical shape to validate against, and `type: 'text'` is the one type that never
1376
- * reaches a stylesheet, which is what makes accepting arbitrary path characters safe.
1377
- */
1378
- src: { type: "text", default: "", cssProperty: "--kui-src" },
1379
- /*
1380
- * The still a `<video>` shows before its first frame decodes. Not optional polish: without it a
1381
- * background clip paints as an empty box for as long as the network takes, and that box is the
1382
- * backdrop to the author's text — the one place on the page where a flash of nothing is most
1383
- * visible. Every background video in this repo's own demo pages is authored with one.
1384
- */
1385
- poster: { type: "text", default: "", cssProperty: "--kui-poster" },
1386
- /*
1387
- * `fill`, `none` and `scale-down` are deliberately absent. `fill` is the only `object-fit` value
1388
- * that distorts — it stretches the picture to the box rather than cropping it — and the standing
1389
- * rule for imagery in this project is to crop, never stretch. The other two leave the media at
1390
- * its intrinsic size inside a box sized to something else, which for a *backdrop* is a gap, not
1391
- * a layout. Adding them would be offering three ways to get a broken background.
1392
- */
1393
- fit: {
1394
- type: "keyword",
1395
- default: "cover",
1396
- cssProperty: "--kui-fit",
1397
- values: ["cover", "contain"]
1398
- },
1399
- /*
1400
- * Which part of the picture a `cover` crop keeps. Nine named points rather than a free
1401
- * `object-position` string, because a free string would have to be `type: 'text'` — the one type
1402
- * that is explicitly never written to a stylesheet (see `core/params.ts`) — and this value is
1403
- * written to one. A keyword list is validated against its own `values`, so the author gets real
1404
- * focal control and the CSS surface stays closed.
1405
- */
1406
- focus: {
1407
- type: "keyword",
1408
- default: "center",
1409
- cssProperty: "--kui-focus",
1410
- values: FOCAL_POINT_NAMES
1411
- },
1412
- /*
1413
- * The scrim. This is the parameter that makes the whole effect usable, because the point of a
1414
- * backdrop here is animated text on top of it, and text over unmodified footage is illegible
1415
- * about half the time — a light frame arrives and the headline vanishes for those seconds.
1416
- *
1417
- * `type: 'color'` so it goes through the same validator every other colour does. `transparent`
1418
- * as the default rather than an empty string for the same reason: `''` is not a colour, and a
1419
- * default that its own type would reject is a lie the schema cannot catch. It is also the honest
1420
- * spelling of "no scrim", and no scrim node is created for it.
1421
- */
1422
- overlay: { type: "color", default: "transparent", cssProperty: "--kui-overlay" },
1423
- /*
1424
- * Separate from the colour rather than folded into it. `overlay:rgb(0 0 0 / 45%)` does parse —
1425
- * the tokenizer is paren-aware — but `overlay:black overlay-opacity:45%` is the spelling someone
1426
- * reaches for while tuning legibility, and tuning is exactly what this value is for.
1427
- */
1428
- "overlay-opacity": { type: "percentage", default: "100%", cssProperty: "--kui-overlay-opacity" },
1429
- /*
1430
- * The opt-out for the play-while-visible behaviour. `in-view` pairs the clip with the viewport
1431
- * and is right for a long section. `always` is for a short hero clip that must never be caught
1432
- * mid-stall by a visibility heuristic. `never` installs the clip and leaves it on its poster,
1433
- * which is also where any mode lands under a reduced-motion preference.
1434
- */
1435
- autoplay: {
1436
- type: "keyword",
1437
- default: "in-view",
1438
- cssProperty: "--kui-autoplay",
1439
- values: ["in-view", "always", "never"]
1440
- },
1441
- /*
1442
- * Bounded at both ends: `0` is a clip that is loaded, decoding, and permanently frozen — worse
1443
- * than `autoplay:never`, which at least says so — and browsers stop honouring rates past roughly
1444
- * 4 anyway, so a larger number is a silent no-op rather than a faster clip.
1445
- */
1446
- rate: {
1447
- type: "number",
1448
- default: "1",
1449
- cssProperty: "--kui-rate",
1450
- finite: true,
1451
- minimum: 0.25,
1452
- maximum: 4
1453
- },
1454
- loop: { type: "keyword", default: "true", cssProperty: "--kui-loop", values: ["true", "false"] }
1455
- /*
1456
- * There is deliberately no `controls:`. The layer this primitive builds paints at `z-index: -1`
1457
- * behind the author's own children, so a native control bar there is focusable by keyboard and
1458
- * occluded by whatever the page happens to put over it — a player you can tab into and cannot
1459
- * see. A clip meant to be controlled is a content `<video controls>` the author writes, not a
1460
- * background one.
1461
- */
1462
- };
1463
- function prepareBackgroundMedia(el, params, ctx) {
1464
- const authored = params.text("src");
1465
- if (!authored) {
1466
- ctx.warn('background-media needs a "src:" \u2014 nothing installed');
1467
- return () => {
1468
- };
1469
- }
1470
- const src = mediaSource(authored, "src", ctx);
1471
- if (!src) return () => {
1472
- };
1473
- const node = el;
1474
- if (ctx.win.getComputedStyle(node).position === "static") ctx.style.set("position", "relative");
1475
- ctx.style.set("isolation", "isolate");
1476
- const layer = installBackgroundMedia(el, ctx, {
1477
- src,
1478
- poster: mediaSource(params.text("poster"), "poster", ctx),
1479
- fit: params.is("fit", "contain") ? "contain" : "cover",
1480
- position: focalPosition(params.text("focus", "center")),
1481
- overlay: params.text("overlay", "transparent"),
1482
- // `num` returns a percentage as a 0–1 ratio, which is exactly what `opacity` takes.
1483
- overlayOpacity: Math.min(1, Math.max(0, params.num("overlay-opacity", 1))),
1484
- autoplay: params.text("autoplay", "in-view"),
1485
- rate: params.num("rate", 1),
1486
- // `!is('loop', 'false')`, not `is('loop')`. Every other read here names its own fallback, and
1487
- // this one has to as well: `is()` takes no fallback argument, so a bare `is('loop')` is only
1488
- // true when something already filled the schema default in. That holds on the animator's path
1489
- // (`readEffectParams` pre-fills every declared parameter) and not on `createParams`, so the
1490
- // positive spelling silently defaulted a true-by-default parameter to false for any caller
1491
- // handing over raw values. Reading it as "loop unless explicitly told not to" states the
1492
- // default at the point of use, where it cannot drift.
1493
- loop: !params.is("loop", "false"),
1494
- reducedMotion: ctx.reducedMotion
1495
- });
1496
- return continuousSetup(layer.remove);
1497
- }
1498
- var MEDIA_JS_PRIMITIVES = [
1499
- {
1500
- id: "slat-assemble",
1501
- renderer: "javascript",
1502
- channels: [CHANNEL.opacity, CHANNEL.translate, CHANNEL.rotate],
1503
- parameters: slatParams,
1504
- supportedTimelines: ["time"],
1505
- supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
1506
- defaultActivation: "enter",
1507
- perfClass: "dom-transform",
1508
- // Same reasoning as every JS-rendered primitive in `text.ts`: nothing here declares a CSS
1509
- // `animation-duration` the reduced-motion policy layer could shorten, and `disable` is what
1510
- // stops `installSlatStage`'s DOM surgery from ever running at all under reduced motion — the
1511
- // animator never calls `activate()`, so the wrapped `<img>` is simply left exactly as authored.
1512
- reducedMotion: "disable",
1513
- // Land()ing hands the picture back to the real `<img>` and tears down every slat — see
1514
- // `prepareSlatAssemble`'s `land()`. Declared, not assumed: see `restoresOnFinish`'s own comment
1515
- // in `core/types.ts` for why the catalog's default is the opposite of this.
1516
- restoresOnFinish: true,
1517
- prepare: deferPrepare(prepareSlatAssemble)
1518
- },
1519
- {
1520
- id: "background-media",
1521
- /*
1522
- * `media` is the same word `media-scrub` uses for "this effect owns what the element shows",
1523
- * and it is what makes `background-media, video-scrub` on one element a reported conflict
1524
- * rather than two effects silently fighting over the same picture.
1525
- *
1526
- * `layout` is claimed for the same reason `pin` claims it: preparation writes `position` and
1527
- * `isolation` on the *host*, which is a stacking-context claim on someone else's element. Left
1528
- * undeclared, `background-media, pin-section` composed silently while both decided what
1529
- * `position` the host has — the conflict detector cannot report a claim it was never told about.
1530
- */
1531
- channels: ["media", "layout"],
1532
- renderer: "javascript",
1533
- parameters: backgroundMediaParams,
1534
- // Not a claim to support four timelines — an abstention. A backdrop is not driven by progress
1535
- // of any kind and this primitive never reads `Timeline`; the list exists only so that
1536
- // `data-kui="background-media src:/hero.mp4, parallax"` plus a `timeline:view` survives
1537
- // `compile.ts`'s `intersect`. See `TIMELINE_AGNOSTIC` (`effects/shared.ts`), shared with the
1538
- // scroll-mechanics drivers, which abstain for the same reason.
1539
- supportedTimelines: TIMELINE_AGNOSTIC,
1540
- supportedActivations: ["load", "enter", "manual"],
1541
- /*
1542
- * `'load'`, not the catalog's usual `'enter'`, and this is the difference between working and
1543
- * not. A backdrop is the element's appearance, so gating it on an IntersectionObserver means a
1544
- * section that is already on screen at page load, one in a background tab (no IO callbacks
1545
- * fire at all until the tab is foregrounded), or one whose own box is still zero-area waits an
1546
- * unbounded time to have any background — and unlike a missed reveal, that is a visibly broken
1547
- * page. An author who *wants* a heavy clip deferred can still write `on:enter`.
1548
- */
1549
- defaultActivation: "load",
1550
- perfClass: "paint",
1551
- /*
1552
- * `'shorten'`, unlike every other JS-rendered primitive in this file and in `text.ts`, and
1553
- * deliberately so. `'disable'` means the animator never calls `activate()` under a reduced
1554
- * motion preference, which for an animation is exactly right and for this is not: it would
1555
- * leave the element with no backdrop at all rather than a calmer one. There is no CSS duration
1556
- * here for `'shorten'` to shorten, so the policy is inert and the effect installs normally;
1557
- * `ctx.reducedMotion` is then read inside, where it suppresses the one genuinely motion-y part
1558
- * — a clip's autoplay — and leaves the poster frame standing. See `autoplayInView`.
1559
- */
1560
- reducedMotion: "shorten",
1561
- prepare: deferPrepare(prepareBackgroundMedia)
1562
- }
1563
- ];
1564
- var MEDIA_JS_PRESETS = [
1565
- { name: "slat-assemble", primitive: "slat-assemble", cloak: true },
1566
- /*
1567
- * Two names, one primitive — the alias shape the catalog already uses everywhere (`pin-until`,
1568
- * `pin-spacer` and `stacking-cards` are three names over the one `pin` primitive; six `wipe-*`
1569
- * names share `media-wipe`). A preset row is the alias mechanism, so a second spelling costs a
1570
- * table entry and nothing else: no duplicated implementation to keep in sync, and both names
1571
- * resolve to the same `prepare`.
1572
- *
1573
- * No `cloak` on either: the pre-JS cloak rule hides an element until the runtime installs the
1574
- * effect's from-state, and this element is the author's own content. Cloaking it would blank
1575
- * their text for as long as the bundle takes to arrive, to hide a backdrop that has no
1576
- * from-state at all.
1577
- */
1578
- { name: "bg", primitive: "background-media" },
1579
- { name: "background", primitive: "background-media" }
1580
- ];
1581
- var MEDIA_PRIMITIVES = [...MEDIA_CSS_PRIMITIVES, ...MEDIA_JS_PRIMITIVES];
1582
- var MEDIA_PRESETS = [...MEDIA_CSS_PRESETS, ...MEDIA_JS_PRESETS];
1583
- function registerMedia(registry) {
1584
- return registry.registerPrimitives(MEDIA_PRIMITIVES).registerPresets(MEDIA_PRESETS);
1585
- }
1586
-
1587
- // src/effects/catalog/subtree-capture.ts
1588
- function captureChildren(el) {
1589
- const authored = Array.from(el.childNodes);
1590
- return () => {
1591
- el.replaceChildren(...authored);
1592
- };
1593
- }
1594
-
1595
- // src/effects/catalog/numbers-shared.ts
1596
- var SR_ONLY_CLASS = "kui-sr-only";
1597
- var DECORATIVE_CLASS = "kui-count-decorative";
1598
- function installCountLayers(el, doc) {
1599
- const restoreChildren = captureChildren(el);
1600
- const decorative = doc.createElement("span");
1601
- decorative.setAttribute("aria-hidden", "true");
1602
- decorative.className = DECORATIVE_CLASS;
1603
- const srOnly = doc.createElement("span");
1604
- srOnly.className = SR_ONLY_CLASS;
1605
- srOnly.setAttribute("aria-live", "polite");
1606
- el.textContent = "";
1607
- el.append(decorative, srOnly);
1608
- return {
1609
- decorative,
1610
- srOnly,
1611
- restore: restoreChildren
1612
- };
1613
- }
1614
- function easeOutCubic(t) {
1615
- const clamped = Math.min(Math.max(t, 0), 1);
1616
- return 1 - (1 - clamped) ** 3;
1617
- }
1618
- function tweenValue(t, from, to) {
1619
- return from + (to - from) * t;
1620
- }
1621
- function cubicBezier(x1, y1, x2, y2) {
1622
- const cx = 3 * x1;
1623
- const bx = 3 * (x2 - x1) - cx;
1624
- const ax = 1 - cx - bx;
1625
- const cy = 3 * y1;
1626
- const by = 3 * (y2 - y1) - cy;
1627
- const ay = 1 - cy - by;
1628
- const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
1629
- const sampleY = (t) => ((ay * t + by) * t + cy) * t;
1630
- const slopeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
1631
- function solveT(x) {
1632
- let t = x;
1633
- for (let i = 0; i < 8; i++) {
1634
- const slope = slopeX(t);
1635
- if (Math.abs(slope) < 1e-6) break;
1636
- const next = t - (sampleX(t) - x) / slope;
1637
- if (Math.abs(sampleX(next) - x) < 1e-6) return next;
1638
- t = next;
1639
- }
1640
- let lo = 0;
1641
- let hi = 1;
1642
- t = x;
1643
- while (hi - lo > 1e-6) {
1644
- if (sampleX(t) < x) lo = t;
1645
- else hi = t;
1646
- t = (lo + hi) / 2;
1647
- }
1648
- return t;
1649
- }
1650
- return (t) => sampleY(solveT(Math.min(Math.max(t, 0), 1)));
1651
- }
1652
- var EASING_KEYWORDS = {
1653
- ease: [0.25, 0.1, 0.25, 1],
1654
- "ease-in": [0.42, 0, 1, 1],
1655
- "ease-out": [0, 0, 0.58, 1],
1656
- "ease-in-out": [0.42, 0, 0.58, 1],
1657
- "expo-in": [0.7, 0, 0.84, 0],
1658
- "expo-out": [0.16, 1, 0.3, 1],
1659
- "expo-in-out": [0.87, 0, 0.13, 1],
1660
- "back-in": [0.36, 0, 0.66, -0.56],
1661
- "back-out": [0.34, 1.56, 0.64, 1],
1662
- "back-in-out": [0.68, -0.6, 0.32, 1.6],
1663
- "quart-out": [0.25, 1, 0.5, 1],
1664
- "circ-out": [0, 0.55, 0.45, 1]
1665
- };
1666
- var CUBIC_BEZIER_FN = /^cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)$/i;
1667
- function resolveEasing(easing, warn) {
1668
- if (easing === void 0) return easeOutCubic;
1669
- if (easing === "linear") return (t) => Math.min(Math.max(t, 0), 1);
1670
- const keyword = EASING_KEYWORDS[easing];
1671
- if (keyword) return cubicBezier(...keyword);
1672
- const fn = CUBIC_BEZIER_FN.exec(easing);
1673
- if (fn) return cubicBezier(Number(fn[1]), Number(fn[2]), Number(fn[3]), Number(fn[4]));
1674
- warn(`easing "${easing}" has no JS equivalent for a counter tween \u2014 using the default ease-out`);
1675
- return easeOutCubic;
1676
- }
1677
- var COMPACT_OPTIONS = { notation: "compact", maximumFractionDigits: 1 };
1678
- function formatCount(value, options) {
1679
- const { format, decimals, currency } = options;
1680
- if (format === "currency") {
1681
- return new Intl.NumberFormat(void 0, {
1682
- style: "currency",
1683
- currency,
1684
- minimumFractionDigits: decimals,
1685
- maximumFractionDigits: decimals
1686
- }).format(value);
1687
- }
1688
- if (format === "percent") {
1689
- return new Intl.NumberFormat(void 0, {
1690
- style: "percent",
1691
- minimumFractionDigits: decimals,
1692
- maximumFractionDigits: decimals
1693
- }).format(value);
1694
- }
1695
- if (format === "compact") {
1696
- return new Intl.NumberFormat(void 0, COMPACT_OPTIONS).format(value);
1697
- }
1698
- return new Intl.NumberFormat(void 0, {
1699
- minimumFractionDigits: decimals,
1700
- maximumFractionDigits: decimals
1701
- }).format(value);
1702
- }
1703
- function paddedDigits(value, width) {
1704
- return Math.round(Math.max(0, value)).toString().padStart(width, "0");
1705
- }
1706
- function groupDigits(digits) {
1707
- const groups = [];
1708
- for (let end = digits.length; end > 0; end -= 3) {
1709
- groups.unshift(digits.slice(Math.max(0, end - 3), end));
1710
- }
1711
- return groups.join(",");
1712
- }
1713
- function odometerTokens(grouped) {
1714
- return [...grouped].map((char) => ({ char, digit: char >= "0" && char <= "9" }));
1715
- }
1716
-
1717
- // src/effects/catalog/numbers.ts
1718
- var COUNT_STEP_MS = 16;
1719
- var REDUCED_MOTION_DURATION_MS = 1;
1720
- var countParams = {
1721
- duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
1722
- ...TRIGGER_DELAY_PARAM,
1723
- from: { type: "number", default: "0", cssProperty: "--kui-from" },
1724
- to: { type: "number", default: "100", cssProperty: "--kui-to" },
1725
- decimals: {
1726
- type: "number",
1727
- default: "0",
1728
- cssProperty: "--kui-decimals",
1729
- integer: true,
1730
- minimum: 0,
1731
- maximum: 6
1732
- },
1733
- format: {
1734
- type: "keyword",
1735
- default: "number",
1736
- cssProperty: "--kui-format",
1737
- values: ["number", "currency", "percent", "compact"]
1738
- },
1739
- currency: { type: "text", default: "USD", cssProperty: "--kui-currency" }
1740
- };
1741
- var odometerParams = {
1742
- duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
1743
- ...TRIGGER_DELAY_PARAM,
1744
- from: { type: "number", default: "0", cssProperty: "--kui-from" },
1745
- to: { type: "number", default: "100", cssProperty: "--kui-to" }
1746
- };
1747
- function countPrimitive(id, parameters, prepare) {
1748
- return {
1749
- id,
1750
- renderer: "javascript",
1751
- channels: ["content"],
1752
- parameters,
1753
- supportedTimelines: ["time"],
1754
- supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
1755
- defaultActivation: "enter",
1756
- perfClass: "dom-transform",
1757
- // Not 'disable': the animator would then skip activation entirely under reduced motion,
1758
- // leaving the counter permanently blank. `prepareTween` reads `ctx.reducedMotion` itself and
1759
- // collapses the ramp to one effectively-instant tick instead, so the final value still lands.
1760
- reducedMotion: "shorten",
1761
- prepare
1762
- };
1763
- }
1764
- function tweenDurationMs(params, ctx) {
1765
- return ctx.reducedMotion ? REDUCED_MOTION_DURATION_MS : Math.max(0, effectDurationMs(params, 1600));
1766
- }
1767
- function tweenTimingFor(params, ctx) {
1768
- return {
1769
- durationMs: tweenDurationMs(params, ctx),
1770
- // Positional first, then the same-named parameter — the two-spellings rule `effectDurationMs`
1771
- // applies to `duration` just above, now applied to `delay` as well.
1772
- delayMs: Math.max(0, params.timing.delayMs ?? params.ms("delay", 0)),
1773
- easing: resolveEasing(params.timing.easing, ctx.warn)
1774
- };
1775
- }
1776
- function tweenNumber(ctx, tween) {
1777
- const { from, to, durationMs, delayMs, easing, onTick } = tween;
1778
- let elapsed = 0;
1779
- let handle;
1780
- let settle2;
1781
- const finished = new Promise((resolve) => {
1782
- settle2 = resolve;
1783
- });
1784
- const stop = () => {
1785
- ctx.win.clearTimeout(start);
1786
- if (handle !== void 0) ctx.win.clearInterval(handle);
1787
- };
1788
- const tick = () => {
1789
- elapsed += COUNT_STEP_MS;
1790
- const ratio = durationMs <= 0 ? 1 : Math.min(elapsed / durationMs, 1);
1791
- const done = ratio >= 1;
1792
- onTick(tweenValue(easing(ratio), from, to), done);
1793
- if (done) {
1794
- stop();
1795
- settle2();
1796
- }
1797
- };
1798
- const start = ctx.win.setTimeout(() => {
1799
- handle = ctx.win.setInterval(tick, COUNT_STEP_MS);
1800
- }, delayMs);
1801
- return {
1802
- cleanup: stop,
1803
- finished,
1804
- finish: () => {
1805
- stop();
1806
- onTick(to, true);
1807
- settle2();
1808
- }
1809
- };
1810
- }
1811
- function prepareCount(el, params, ctx) {
1812
- const doc = el.ownerDocument;
1813
- const from = params.num("from", 0);
1814
- const to = params.num("to", 100);
1815
- const decimals = Math.max(0, Math.round(params.num("decimals", 0)));
1816
- const format = params.text("format", "number");
1817
- const currency = params.text("currency", "USD");
1818
- const options = { format, decimals, currency };
1819
- const layers = installCountLayers(el, doc);
1820
- layers.decorative.textContent = formatCount(from, options);
1821
- layers.srOnly.textContent = formatCount(from, options);
1822
- const tween = tweenNumber(ctx, {
1823
- from,
1824
- to,
1825
- ...tweenTimingFor(params, ctx),
1826
- onTick: (value, done) => {
1827
- layers.decorative.textContent = formatCount(value, options);
1828
- if (done) layers.srOnly.textContent = formatCount(to, options);
1829
- }
1830
- });
1831
- return {
1832
- cleanup: () => {
1833
- tween.cleanup();
1834
- layers.restore();
1835
- },
1836
- finished: tween.finished,
1837
- finish: tween.finish
1838
- };
1839
- }
1840
- function buildOdometerColumns(container, doc, grouped) {
1841
- const strips = [];
1842
- for (const token of odometerTokens(grouped)) {
1843
- if (!token.digit) {
1844
- container.append(doc.createTextNode(token.char));
1845
- continue;
1846
- }
1847
- const column = doc.createElement("span");
1848
- column.className = "kui-odometer-col";
1849
- const strip = doc.createElement("span");
1850
- strip.className = "kui-odometer-strip";
1851
- for (let digit = 0; digit <= 9; digit++) {
1852
- const row = doc.createElement("span");
1853
- row.textContent = String(digit);
1854
- strip.append(row);
1855
- }
1856
- strip.style.setProperty("--kui-o", token.char);
1857
- column.append(strip);
1858
- container.append(column);
1859
- strips.push(strip);
1860
- }
1861
- return strips;
1862
- }
1863
- function updateOdometerColumns(strips, grouped) {
1864
- let index = 0;
1865
- for (const token of odometerTokens(grouped)) {
1866
- if (!token.digit) continue;
1867
- strips[index]?.style.setProperty("--kui-o", token.char);
1868
- index++;
1869
- }
1870
- }
1871
- function prepareOdometer(el, params, ctx) {
1872
- const doc = el.ownerDocument;
1873
- const from = Math.max(0, params.num("from", 0));
1874
- const to = Math.max(0, params.num("to", 100));
1875
- const width = Math.max(String(Math.round(from)).length, String(Math.round(to)).length);
1876
- const toGrouped = groupDigits(paddedDigits(to, width));
1877
- const fromGrouped = groupDigits(paddedDigits(from, width));
1878
- const layers = installCountLayers(el, doc);
1879
- layers.srOnly.textContent = fromGrouped;
1880
- const strips = buildOdometerColumns(layers.decorative, doc, fromGrouped);
1881
- const tween = tweenNumber(ctx, {
1882
- from,
1883
- to,
1884
- ...tweenTimingFor(params, ctx),
1885
- onTick: (value, done) => {
1886
- updateOdometerColumns(strips, groupDigits(paddedDigits(value, width)));
1887
- if (done) layers.srOnly.textContent = toGrouped;
1888
- }
1889
- });
1890
- return {
1891
- cleanup: () => {
1892
- tween.cleanup();
1893
- layers.restore();
1894
- },
1895
- finished: tween.finished,
1896
- finish: tween.finish
1897
- };
1898
- }
1899
- var COUNT_PRIMITIVES = [
1900
- countPrimitive("count", countParams, deferPrepare(prepareCount)),
1901
- countPrimitive("count-odometer", odometerParams, deferPrepare(prepareOdometer))
1902
- ];
1903
- var COUNT_PRESETS = [
1904
- { name: "count-up", primitive: "count", params: { from: "0", to: "100" } },
1905
- { name: "count-down", primitive: "count", params: { from: "100", to: "0" } },
1906
- {
1907
- name: "count-currency",
1908
- primitive: "count",
1909
- params: { from: "0", to: "4820", format: "currency", currency: "USD", decimals: "0" }
1910
- },
1911
- {
1912
- name: "count-percent",
1913
- primitive: "count",
1914
- params: { from: "0", to: "0.82", format: "percent", decimals: "0" }
1915
- },
1916
- {
1917
- name: "count-compact",
1918
- primitive: "count",
1919
- params: { from: "0", to: "128400", format: "compact" }
1920
- },
1921
- { name: "odometer-roll", primitive: "count-odometer", params: { from: "0", to: "4820" } }
1922
- ];
1923
- var METER_PRIMITIVES = [
1924
- cssPrimitive("stroke-sweep", [CHANNEL.stroke]),
1925
- // `from` gives `progress-bar` a real knob on its start scale — it had none before. `--kui-bar-from`
1926
- // is also what the `[data-kui-fx~='progress-bar'][data-kui-state='ready']` gate in numbers.css
1927
- // neutralizes; see that rule's comment for the on:enter fix this parameter doubles as.
1928
- cssPrimitive("meter-bar", [CHANNEL.scale], {
1929
- parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
1930
- }),
1931
- cssPrimitive("meter-segments", [CHANNEL.opacity]),
1932
- cssPrimitive("meter-stars", [CHANNEL.clip])
1933
- ];
1934
- var METER_PRESETS = [
1935
- { name: "progress-ring", primitive: "stroke-sweep", keyframes: "kui-progress-ring" },
1936
- { name: "gauge-sweep", primitive: "stroke-sweep", keyframes: "kui-gauge-sweep" },
1937
- { name: "donut-sweep", primitive: "stroke-sweep", keyframes: "kui-donut-sweep" },
1938
- { name: "sparkline-draw", primitive: "stroke-sweep", keyframes: "kui-sparkline-draw" },
1939
- // `cloak: true`: `kui-progress-bar`'s `from { scale: 0 1 }` (numbers.css) is a zero-width box
1940
- // while paused, not just an invisible one — so while it waits it occupies no space in layout at
1941
- // all. Not, despite the tidier story, because an observer refuses to fire on it: Chromium was
1942
- // measured resolving a zero-area target's `intersectionRatio` to `1`. See numbers.css's
1943
- // `[data-kui-fx~='progress-bar'][data-kui-state='ready']` rule for the geometry half of the fix;
1944
- // `cloak` keeps the pre-JS and post-JS "waiting" look the same (invisible) either side of it.
1945
- { name: "progress-bar", primitive: "meter-bar", keyframes: "kui-progress-bar", cloak: true },
1946
- { name: "progress-segments", primitive: "meter-segments", keyframes: "kui-progress-segments" },
1947
- { name: "star-rating-fill", primitive: "meter-stars", keyframes: "kui-star-rating-fill" }
1948
- ];
1949
- var NUMBERS_PRIMITIVES = [...COUNT_PRIMITIVES, ...METER_PRIMITIVES];
1950
- var NUMBERS_PRESETS = [...COUNT_PRESETS, ...METER_PRESETS];
1951
- function registerNumbers(registry) {
1952
- return registry.registerPrimitives(NUMBERS_PRIMITIVES).registerPresets(NUMBERS_PRESETS);
1953
- }
1954
-
1955
- // src/effects/catalog/text-shared.ts
1956
- var SR_ONLY_CLASS2 = "kui-sr-only";
1957
- var DECORATIVE_CLASS2 = "kui-split-decorative";
1958
- function installSplitLayers(el, doc) {
1959
- const originalText = el.textContent.trim();
1960
- const restoreChildren = captureChildren(el);
1961
- const decorative = doc.createElement("span");
1962
- decorative.setAttribute("aria-hidden", "true");
1963
- decorative.className = DECORATIVE_CLASS2;
1964
- const srOnly = doc.createElement("span");
1965
- srOnly.className = SR_ONLY_CLASS2;
1966
- srOnly.textContent = originalText;
1967
- el.textContent = "";
1968
- el.append(decorative, srOnly);
1969
- return {
1970
- decorative,
1971
- originalText,
1972
- restore: restoreChildren
1973
- };
1974
- }
1975
- function segmentGraphemes(text) {
1976
- const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1977
- return Array.from(segmenter.segment(text), (entry) => entry.segment);
1978
- }
1979
- function segmentWords(text) {
1980
- const segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
1981
- return Array.from(segmenter.segment(text), (entry) => ({
1982
- text: entry.segment,
1983
- isWord: entry.isWordLike === true
1984
- }));
1985
- }
1986
- function markItem(el, index, extraClass) {
1987
- el.className = extraClass ? `kui-split-item ${extraClass}` : "kui-split-item";
1988
- el.style.setProperty("--kui-i", String(index));
1989
- }
1990
- function applyStaggerVars(el, params) {
1991
- const { durationMs, delayMs, easing } = params.timing;
1992
- const duration = durationMs === void 0 ? params.text("duration", "500ms") : `${durationMs}ms`;
1993
- const delay = delayMs === void 0 ? params.text("delay", "0ms") : `${delayMs}ms`;
1994
- el.style.setProperty("--kui-duration", duration);
1995
- el.style.setProperty("--kui-delay", delay);
1996
- el.style.setProperty("--kui-ease", easing ?? params.text("ease", "ease-out"));
1997
- el.style.setProperty("--kui-stagger", params.text("stagger", "30ms"));
1998
- }
1999
- function stepMsFor(params, ticks, fallback) {
2000
- const total = params.timing.durationMs;
2001
- if (total === void 0 || ticks <= 0) return params.ms("step", fallback);
2002
- return Math.max(1, total / ticks);
2003
- }
2004
- function splitRevealFinishMs(params, itemCount) {
2005
- if (itemCount === 0) return 0;
2006
- const durationMs = params.timing.durationMs ?? params.ms("duration", 500);
2007
- const delayMs = params.timing.delayMs ?? params.ms("delay", 0);
2008
- const staggerMs = params.ms("stagger", 30);
2009
- return delayMs + (itemCount - 1) * staggerMs + durationMs;
2010
- }
2011
- function createStepRunner(win, options) {
2012
- let settle2;
2013
- const finished = new Promise((resolve) => {
2014
- settle2 = resolve;
2015
- });
2016
- let interval;
2017
- function stop() {
2018
- win.clearTimeout(start);
2019
- if (interval !== void 0) win.clearInterval(interval);
2020
- settle2?.();
2021
- }
2022
- const start = win.setTimeout(() => {
2023
- interval = win.setInterval(() => {
2024
- if (options.tick()) stop();
2025
- }, options.stepMs);
2026
- }, options.delayMs);
2027
- return { finished, stop };
2028
- }
2029
- function appendCharSpans(container, doc, text) {
2030
- const spans = [];
2031
- let index = 0;
2032
- let wordWrapper = null;
2033
- for (const grapheme of segmentGraphemes(text)) {
2034
- if (grapheme.trim() === "") {
2035
- container.append(doc.createTextNode(grapheme));
2036
- wordWrapper = null;
2037
- continue;
2038
- }
2039
- if (!wordWrapper) {
2040
- wordWrapper = doc.createElement("span");
2041
- wordWrapper.className = "kui-split-word";
2042
- container.append(wordWrapper);
2043
- }
2044
- const span = doc.createElement("span");
2045
- markItem(span, index);
2046
- span.textContent = grapheme;
2047
- wordWrapper.append(span);
2048
- spans.push(span);
2049
- index++;
2050
- }
2051
- return spans;
2052
- }
2053
- function appendWordSpans(container, doc, text) {
2054
- const spans = [];
2055
- let index = 0;
2056
- for (const token of segmentWords(text)) {
2057
- if (!token.isWord) {
2058
- container.append(doc.createTextNode(token.text));
2059
- continue;
2060
- }
2061
- const span = doc.createElement("span");
2062
- markItem(span, index);
2063
- span.textContent = token.text;
2064
- container.append(span);
2065
- spans.push(span);
2066
- index++;
2067
- }
2068
- return spans;
2069
- }
2070
- function bucketByLine(container) {
2071
- const buckets = [];
2072
- let currentTop = null;
2073
- for (const node of Array.from(container.childNodes)) {
2074
- if (node instanceof HTMLElement) {
2075
- const top = node.offsetTop;
2076
- if (currentTop === null || Math.abs(top - currentTop) > 1) {
2077
- buckets.push([]);
2078
- currentTop = top;
2079
- }
2080
- } else if (buckets.length === 0) {
2081
- buckets.push([]);
2082
- }
2083
- buckets.at(-1).push(node);
2084
- }
2085
- return buckets;
2086
- }
2087
- function appendLineSpans(container, doc, text) {
2088
- appendWordSpans(container, doc, text);
2089
- const buckets = bucketByLine(container);
2090
- container.replaceChildren();
2091
- return buckets.map((nodes, index) => {
2092
- const line = doc.createElement("span");
2093
- markItem(line, index, "kui-split-line");
2094
- for (const node of nodes) {
2095
- if (node instanceof HTMLElement) {
2096
- node.removeAttribute("class");
2097
- node.style.removeProperty("--kui-i");
2098
- }
2099
- line.append(node);
2100
- }
2101
- container.append(line);
2102
- return line;
2103
- });
2104
- }
2105
- function appendSpansFor(unit, container, doc, text) {
2106
- if (unit === "words") return appendWordSpans(container, doc, text);
2107
- if (unit === "lines") return appendLineSpans(container, doc, text);
2108
- return appendCharSpans(container, doc, text);
2109
- }
2110
- function nextTypeState(state, total, loop2) {
2111
- if (!state.deleting) {
2112
- const index2 = state.index + 1;
2113
- if (index2 < total) return { index: index2, deleting: false, done: false };
2114
- return loop2 ? { index: total, deleting: true, done: false } : { index: total, deleting: false, done: true };
2115
- }
2116
- const index = state.index - 1;
2117
- if (index > 0) return { index, deleting: true, done: false };
2118
- return { index: 0, deleting: false, done: false };
2119
- }
2120
- var SCRAMBLE_CHARSETS = {
2121
- upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
2122
- binary: "01",
2123
- symbols: "!<>-_\\/[]{}=+*^?#~"
2124
- };
2125
- function scrambledFrame(graphemes, resolved, charset, random) {
2126
- return graphemes.map((grapheme, index) => {
2127
- if (index < resolved || grapheme.trim() === "") return grapheme;
2128
- return charset[Math.floor(random() * charset.length)] ?? charset[0] ?? grapheme;
2129
- }).join("");
2130
- }
2131
-
2132
- // src/effects/catalog/text.ts
2133
- var fontWeightParams = {
2134
- from: { type: "number", default: "100", cssProperty: "--kui-from-weight", minimum: 1, maximum: 1e3 },
2135
- to: { type: "number", default: "800", cssProperty: "--kui-to-weight", minimum: 1, maximum: 1e3 }
2136
- };
2137
- var fontWidthParams = {
2138
- from: { type: "percentage", default: "75%", cssProperty: "--kui-from-width" },
2139
- to: { type: "percentage", default: "125%", cssProperty: "--kui-to-width" }
2140
- };
2141
- var fontSlantParams = {
2142
- from: { type: "angle", default: "0deg", cssProperty: "--kui-from-slant" },
2143
- to: { type: "angle", default: "10deg", cssProperty: "--kui-to-slant" }
2144
- };
2145
- var textSweepParams = {
2146
- color: { type: "color", default: "currentColor", cssProperty: "--kui-sweep-color" }
2147
- };
2148
- var extrudeParams = {
2149
- angle: { type: "angle", default: "-20deg", cssProperty: "--kui-from-angle" },
2150
- distance: { type: "length", default: "32px", cssProperty: "--kui-distance" }
2151
- };
2152
- var TEXT_CSS_PRIMITIVES = [
2153
- // `color`, alongside `background`: both presets' unconditional rule sets `-webkit-text-fill-color:
2154
- // transparent` so the `background-image` gradient shows through the glyphs (the standard
2155
- // gradient-text technique). That is a real claim on the glyph fill, the same physical property
2156
- // `text-outline-fill` animates on its own `color` channel below — without this, the two looked
2157
- // disjoint to the compiler (`background` vs `stroke`+`color`) and composing them would let
2158
- // whichever applied last silently win the glyph fill instead of being flagged as a conflict.
2159
- cssPrimitive("text-shimmer", [CHANNEL.background, CHANNEL.color], { reducedMotion: "disable" }),
2160
- // `gradient-sweep` is the only one of the three `text-sweep`-family presets whose keyframe
2161
- // touches `-webkit-text-fill-color` (see text.css) — `highlight-sweep` and `underline-draw` only
2162
- // paint a `background-image`. Claiming `color` for all three made the compiler reject compositions
2163
- // like `underline-draw, text-outline-fill` as a glyph-fill conflict even though they touch disjoint
2164
- // properties. Split so only the preset that actually claims the fill declares the channel.
2165
- cssPrimitive("text-gradient-sweep", [CHANNEL.background, CHANNEL.color], { parameters: textSweepParams }),
2166
- cssPrimitive("text-sweep", [CHANNEL.background], { parameters: textSweepParams }),
2167
- cssPrimitive("text-outline-fill", [CHANNEL.stroke, CHANNEL.color]),
2168
- cssPrimitive("var-weight", ["font"], { parameters: fontWeightParams }),
2169
- cssPrimitive("var-width", ["font"], { parameters: fontWidthParams }),
2170
- cssPrimitive("var-slant", ["font"], { parameters: fontSlantParams }),
2171
- /*
2172
- * `defaultActivation: 'load'` for the same reason every ambient primitive declares it, and it
2173
- * was missing here: a marquee is continuous motion, so `reducedMotion: 'disable'` is only half
2174
- * the rule `ambient.ts` spells out — the other half is starting on `load` rather than waiting on
2175
- * a scroll-triggered `enter`.
2176
- *
2177
- * Without it, a bare `data-kui="marquee 42s"` resolved to `enter`, which `style-plan.ts`'s
2178
- * `resolveGate` sends down the `deferred` path and stamps `animation-play-state: paused`. It
2179
- * compiled correctly, reported `data-kui-state="ready"`, and never ran — measured on a marquee
2180
- * fully in view, so this was not an observer that had simply not fired yet. Every page carrying
2181
- * one had to know to write `on:load`, which is exactly the kind of thing an author cannot be
2182
- * expected to guess.
2183
- *
2184
- * `marquee-scroll-linked` is unaffected: its position comes from `animation-timeline: scroll()`,
2185
- * not from an activation.
2186
- */
2187
- cssPrimitive("text-marquee", [CHANNEL.translate], {
2188
- timelines: ["time", "scroll"],
2189
- defaultActivation: "load",
2190
- reducedMotion: "disable"
2191
- }),
2192
- cssPrimitive("redaction-reveal", [CHANNEL.clip]),
2193
- cssPrimitive("text-3d-extrude", [CHANNEL.rotate, CHANNEL.translate], { parameters: extrudeParams })
2194
- ];
2195
- var TEXT_CSS_PRESETS = [
2196
- { name: "gradient-shimmer", primitive: "text-shimmer", keyframes: "kui-gradient-shimmer" },
2197
- { name: "gradient-sweep", primitive: "text-gradient-sweep", keyframes: "kui-gradient-sweep" },
2198
- {
2199
- name: "highlight-sweep",
2200
- primitive: "text-sweep",
2201
- keyframes: "kui-highlight-sweep",
2202
- params: { color: "gold" }
2203
- },
2204
- { name: "underline-draw", primitive: "text-sweep", keyframes: "kui-underline-draw" },
2205
- { name: "text-outline-fill", primitive: "text-outline-fill", keyframes: "kui-text-outline-fill" },
2206
- { name: "var-weight", primitive: "var-weight", keyframes: "kui-var-weight" },
2207
- { name: "var-width", primitive: "var-width", keyframes: "kui-var-width" },
2208
- { name: "var-slant", primitive: "var-slant", keyframes: "kui-var-slant" },
2209
- { name: "marquee", primitive: "text-marquee", keyframes: "kui-marquee" },
2210
- { name: "marquee-scroll-linked", primitive: "text-marquee", keyframes: "kui-marquee" },
2211
- { name: "redaction-reveal", primitive: "redaction-reveal", keyframes: "kui-redaction-reveal" },
2212
- { name: "text-3d-extrude", primitive: "text-3d-extrude", keyframes: "kui-text-3d-extrude" }
2213
- ];
2214
- function jsTextPrimitive(id, channels, options) {
2215
- return {
2216
- id,
2217
- renderer: "javascript",
2218
- channels,
2219
- parameters: options.parameters,
2220
- supportedTimelines: ["time"],
2221
- supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
2222
- defaultActivation: "enter",
2223
- perfClass: options.perfClass ?? "dom-transform",
2224
- // Every JS-rendered primitive in this catalog is `disable`: none of them declare a CSS
2225
- // `animation-duration` the reduced-motion policy layer could shorten, so `shorten` would be a
2226
- // silent no-op. `disable` is enforced upstream — the animator never calls `activate()` at all
2227
- // — which is the only place that actually works for a timer- or DOM-surgery-driven effect.
2228
- reducedMotion: "disable",
2229
- prepare: options.prepare
2230
- };
2231
- }
2232
- var splitTiming = {
2233
- duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
2234
- ...TRIGGER_DELAY_PARAM,
2235
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
2236
- stagger: { type: "time", default: "30ms", cssProperty: "--kui-stagger" },
2237
- unit: {
2238
- type: "keyword",
2239
- default: "chars",
2240
- cssProperty: "--kui-unit",
2241
- values: ["chars", "words", "lines"]
2242
- },
2243
- direction: {
2244
- type: "keyword",
2245
- default: "fade",
2246
- cssProperty: "--kui-direction",
2247
- values: ["fade", "up", "down", "mask"]
2248
- }
2249
- };
2250
- var motionParams = {
2251
- stagger: { type: "time", default: "40ms", cssProperty: "--kui-stagger" },
2252
- // `duration`/`ease` are deliberately *not* declared beside the delay: text.css pins both for
2253
- // wave and jitter on a higher-specificity `[data-kui-split-fx='wave'] .kui-split-item` rule, so
2254
- // declaring them would advertise two knobs that the stylesheet then overrides. `animation-delay`
2255
- // is the one the phase-start rule leaves alone, which is what lets `applyStaggerVars` honour it.
2256
- ...TRIGGER_DELAY_PARAM,
2257
- motion: {
2258
- type: "keyword",
2259
- default: "wave",
2260
- cssProperty: "--kui-motion",
2261
- values: ["wave", "jitter"]
2262
- }
2263
- };
2264
- var typewriterParams = {
2265
- step: { type: "time", default: "55ms", cssProperty: "--kui-step" },
2266
- loop: { type: "keyword", default: "false", cssProperty: "--kui-loop", values: ["true", "false"] },
2267
- ...TRIGGER_DELAY_PARAM
2268
- };
2269
- var scrambleParams = {
2270
- step: { type: "time", default: "40ms", cssProperty: "--kui-step" },
2271
- // `duration` gets no such shared declaration: `stepMsFor` reads its authored-or-not distinction
2272
- // to decide between a whole-effect time and a per-tick `step:`, and a schema default would erase
2273
- // that distinction. A `0ms` delay default has no equivalent problem.
2274
- ...TRIGGER_DELAY_PARAM,
2275
- revealEvery: {
2276
- type: "number",
2277
- default: "2",
2278
- cssProperty: "--kui-reveal-every",
2279
- minimum: 1,
2280
- integer: true
2281
- },
2282
- charset: {
2283
- type: "keyword",
2284
- default: "upper",
2285
- cssProperty: "--kui-charset",
2286
- values: ["upper", "binary", "symbols"]
2287
- }
2288
- };
2289
- var wordCyclerParams = {
2290
- words: { type: "text", default: "", cssProperty: "--kui-words" },
2291
- interval: { type: "time", default: "2200ms", cssProperty: "--kui-interval" },
2292
- // Load-bearing here, not just for symmetry: a cycler has no authored `duration` — `interval:`
2293
- // paces it — so the positional "duration then delay" slot only reached a delay when the author
2294
- // wrote a throwaway first value, `word-cycler 0ms 300ms`.
2295
- ...TRIGGER_DELAY_PARAM
2296
- };
2297
- function prepareSplitText(el, params, ctx) {
2298
- const doc = el.ownerDocument;
2299
- const unit = params.text("unit", "chars");
2300
- const direction = params.text("direction", "fade");
2301
- const layers = installSplitLayers(el, doc);
2302
- layers.decorative.setAttribute("data-kui-split-fx", direction);
2303
- applyStaggerVars(layers.decorative, params);
2304
- const items = appendSpansFor(unit, layers.decorative, doc, layers.originalText);
2305
- let settle2;
2306
- const finished = new Promise((resolve) => {
2307
- settle2 = resolve;
2308
- });
2309
- const timer = ctx.win.setTimeout(settle2, splitRevealFinishMs(params, items.length));
2310
- return {
2311
- cleanup: () => {
2312
- ctx.win.clearTimeout(timer);
2313
- layers.restore();
2314
- },
2315
- finished,
2316
- finish: () => {
2317
- ctx.win.clearTimeout(timer);
2318
- settle2();
2319
- }
2320
- };
2321
- }
2322
- function prepareSplitMotion(el, params) {
2323
- const doc = el.ownerDocument;
2324
- const motion = params.text("motion", "wave");
2325
- const layers = installSplitLayers(el, doc);
2326
- layers.decorative.setAttribute("data-kui-split-fx", motion);
2327
- applyStaggerVars(layers.decorative, params);
2328
- appendCharSpans(layers.decorative, doc, layers.originalText);
2329
- return layers.restore;
2330
- }
2331
- function prepareTypewriter(el, params, ctx) {
2332
- const loop2 = params.is("loop");
2333
- const layers = installSplitLayers(el, el.ownerDocument);
2334
- layers.decorative.classList.add("kui-typewriter");
2335
- const graphemes = segmentGraphemes(layers.originalText);
2336
- let state = { index: 0, deleting: false };
2337
- const render = (count) => {
2338
- layers.decorative.textContent = graphemes.slice(0, count).join("");
2339
- };
2340
- const run = createStepRunner(ctx.win, {
2341
- delayMs: params.timing.delayMs ?? params.ms("delay", 0),
2342
- stepMs: stepMsFor(params, graphemes.length, 55),
2343
- tick: () => {
2344
- const step = nextTypeState(state, graphemes.length, loop2);
2345
- state = step;
2346
- render(step.index);
2347
- return step.done;
2348
- }
2349
- });
2350
- return {
2351
- cleanup: () => {
2352
- run.stop();
2353
- layers.restore();
2354
- },
2355
- finished: run.finished,
2356
- finish: () => {
2357
- run.stop();
2358
- render(graphemes.length);
2359
- }
2360
- };
2361
- }
2362
- function prepareScramble(el, params, ctx) {
2363
- const charset = SCRAMBLE_CHARSETS[params.text("charset", "upper")];
2364
- const revealEvery = Math.max(1, Math.round(params.num("revealEvery", 2)));
2365
- const node = el;
2366
- const authoredMinWidth = node.style.getPropertyValue("min-width");
2367
- const authoredMinHeight = node.style.getPropertyValue("min-height");
2368
- const restRect = el.getBoundingClientRect();
2369
- ctx.style.set("min-width", `${restRect.width}px`);
2370
- ctx.style.set("min-height", `${restRect.height}px`);
2371
- const releaseSizeLock = () => {
2372
- ctx.style.set("min-width", authoredMinWidth);
2373
- ctx.style.set("min-height", authoredMinHeight);
2374
- };
2375
- const layers = installSplitLayers(el, el.ownerDocument);
2376
- layers.decorative.classList.add("kui-scramble");
2377
- const graphemes = segmentGraphemes(layers.originalText);
2378
- let resolved = 0;
2379
- let ticks = 0;
2380
- const render = () => {
2381
- layers.decorative.textContent = scrambledFrame(graphemes, resolved, charset, Math.random);
2382
- };
2383
- render();
2384
- const totalTicks = Math.max(1, graphemes.length * revealEvery);
2385
- const run = createStepRunner(ctx.win, {
2386
- delayMs: params.timing.delayMs ?? params.ms("delay", 0),
2387
- stepMs: stepMsFor(params, totalTicks, Math.max(1, 700 / totalTicks)),
2388
- tick: () => {
2389
- ticks++;
2390
- if (ticks % revealEvery === 0) resolved++;
2391
- render();
2392
- const done = resolved >= graphemes.length;
2393
- if (done) releaseSizeLock();
2394
- return done;
2395
- }
2396
- });
2397
- return {
2398
- cleanup: () => {
2399
- run.stop();
2400
- layers.restore();
2401
- releaseSizeLock();
2402
- },
2403
- finished: run.finished,
2404
- finish: () => {
2405
- run.stop();
2406
- resolved = graphemes.length;
2407
- render();
2408
- releaseSizeLock();
2409
- }
2410
- };
2411
- }
2412
- function prepareWordCycler(el, params, ctx) {
2413
- const words = params.text("words", "").split("|").map((word) => word.trim()).filter(Boolean);
2414
- if (words.length === 0) return () => {
2415
- };
2416
- const restoreChildren = captureChildren(el);
2417
- const swapMs = 150;
2418
- let index = 0;
2419
- el.textContent = words[0];
2420
- const run = createStepRunner(ctx.win, {
2421
- delayMs: params.timing.delayMs ?? params.ms("delay", 0),
2422
- stepMs: params.ms("interval", 2200),
2423
- tick: () => {
2424
- el.classList.add("kui-word-cycler-swap");
2425
- ctx.win.setTimeout(() => {
2426
- index = (index + 1) % words.length;
2427
- el.textContent = words[index];
2428
- el.classList.remove("kui-word-cycler-swap");
2429
- }, swapMs);
2430
- return false;
2431
- }
2432
- });
2433
- return {
2434
- cleanup: () => {
2435
- run.stop();
2436
- el.classList.remove("kui-word-cycler-swap");
2437
- restoreChildren();
2438
- },
2439
- finished: run.finished,
2440
- finish: () => run.stop()
2441
- };
2442
- }
2443
- var TEXT_JS_PRIMITIVES = [
2444
- jsTextPrimitive("split-text", [CHANNEL.opacity, CHANNEL.translate, CHANNEL.clip], {
2445
- parameters: splitTiming,
2446
- prepare: deferPrepare(prepareSplitText)
2447
- }),
2448
- jsTextPrimitive("split-text-motion", [CHANNEL.translate, CHANNEL.rotate], {
2449
- parameters: motionParams,
2450
- prepare: deferPrepare(prepareSplitMotion),
2451
- perfClass: "continuous"
2452
- }),
2453
- jsTextPrimitive("typewriter", [CHANNEL.clip], {
2454
- parameters: typewriterParams,
2455
- prepare: deferPrepare(prepareTypewriter),
2456
- perfClass: "continuous"
2457
- }),
2458
- jsTextPrimitive("scramble-text", ["content"], {
2459
- parameters: scrambleParams,
2460
- prepare: deferPrepare(prepareScramble),
2461
- perfClass: "continuous"
2462
- }),
2463
- jsTextPrimitive("word-cycler", ["content"], {
2464
- parameters: wordCyclerParams,
2465
- prepare: deferPrepare(prepareWordCycler),
2466
- perfClass: "continuous"
2467
- })
2468
- ];
2469
- var CHARS_STAGGER = "30ms";
2470
- var WORDS_STAGGER = "90ms";
2471
- var LINES_STAGGER = "160ms";
2472
- var TEXT_JS_PRESETS = [
2473
- { name: "split-chars", primitive: "split-text", params: { unit: "chars", direction: "fade", stagger: CHARS_STAGGER } },
2474
- { name: "split-words", primitive: "split-text", params: { unit: "words", direction: "fade", stagger: WORDS_STAGGER } },
2475
- { name: "split-lines", primitive: "split-text", params: { unit: "lines", direction: "fade", stagger: LINES_STAGGER } },
2476
- { name: "text-reveal-up", primitive: "split-text", params: { unit: "words", direction: "up", stagger: WORDS_STAGGER }, cloak: true },
2477
- { name: "text-reveal-down", primitive: "split-text", params: { unit: "words", direction: "down", stagger: WORDS_STAGGER }, cloak: true },
2478
- { name: "text-reveal-mask", primitive: "split-text", params: { unit: "lines", direction: "mask", stagger: LINES_STAGGER }, cloak: true },
2479
- { name: "text-wave", primitive: "split-text-motion", params: { motion: "wave" } },
2480
- { name: "text-jitter", primitive: "split-text-motion", params: { motion: "jitter" } },
2481
- { name: "typewriter", primitive: "typewriter", params: { loop: "false" } },
2482
- { name: "typewriter-loop", primitive: "typewriter", params: { loop: "true" } },
2483
- { name: "scramble", primitive: "scramble-text", params: { charset: "upper" } },
2484
- { name: "decode", primitive: "scramble-text", params: { charset: "binary" } },
2485
- { name: "glitch", primitive: "scramble-text", params: { charset: "symbols" } },
2486
- { name: "word-cycler", primitive: "word-cycler" }
2487
- ];
2488
- var TEXT_PRIMITIVES = [...TEXT_CSS_PRIMITIVES, ...TEXT_JS_PRIMITIVES];
2489
- var TEXT_PRESETS = [...TEXT_CSS_PRESETS, ...TEXT_JS_PRESETS];
2490
- function registerText(registry) {
2491
- return registry.registerPrimitives(TEXT_PRIMITIVES).registerPresets(TEXT_PRESETS);
2492
- }
2493
-
2494
- // src/effects/catalog/index.ts
2495
- function registerCatalog(registry) {
2496
- registerMedia(registry);
2497
- registerText(registry);
2498
- registerAmbient(registry);
2499
- registerFeedback(registry);
2500
- registerNumbers(registry);
2501
- registerInteraction(registry);
2502
- return registry;
2503
- }
2504
-
2505
- // src/effects/catalog/core.ts
2506
- var distance = {
2507
- distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
2508
- opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" }
2509
- };
2510
- var ENTRANCE_TIMELINES = ["time", "view", "scroll", "pin"];
2511
- var PRIMITIVES = [
2512
- // --- entrance / exit -------------------------------------------------------------------
2513
- cssPrimitive("reveal", [CHANNEL.opacity, CHANNEL.translate], { timelines: ENTRANCE_TIMELINES, parameters: distance }),
2514
- cssPrimitive("scale", [CHANNEL.scale], {
2515
- timelines: ENTRANCE_TIMELINES,
2516
- parameters: { scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" } }
2517
- }),
2518
- // Separate from `scale` because it claims translate as well and so composes differently.
2519
- // `distance.distance` only, not `...distance`: `kui-zoom-in-up`/`-down` (entrance.css) read
2520
- // `--kui-distance` and `--kui-from-scale` but never `--kui-from-opacity` — this primitive
2521
- // doesn't declare `CHANNEL.opacity`. Spreading the whole shared `distance` object used to expose
2522
- // `opacity:` as an apparently-valid, silently-inert parameter (same dead-parameter shape as
2523
- // `flip-3d`'s old `perspective`, fixed above).
2524
- cssPrimitive("scale-move", [CHANNEL.scale, CHANNEL.translate], {
2525
- timelines: ENTRANCE_TIMELINES,
2526
- parameters: {
2527
- distance: distance.distance,
2528
- scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" }
2529
- }
2530
- }),
2531
- cssPrimitive("rotate", [CHANNEL.rotate], {
2532
- timelines: ENTRANCE_TIMELINES,
2533
- parameters: { angle: { type: "angle", default: "-8deg", cssProperty: "--kui-from-angle" } }
2534
- }),
2535
- // `distance.distance` only, not `...distance`: `kui-roll-in`/`-out` (entrance.css) write
2536
- // `rotate`/`translate`, never `opacity` — this primitive doesn't declare `CHANNEL.opacity`. Same
2537
- // dead-parameter shape as `scale-move` above.
2538
- cssPrimitive("roll", [CHANNEL.rotate, CHANNEL.translate], {
2539
- timelines: ENTRANCE_TIMELINES,
2540
- parameters: {
2541
- distance: distance.distance,
2542
- angle: { type: "angle", default: "-120deg", cssProperty: "--kui-from-angle" }
2543
- }
2544
- }),
2545
- // `skew`, not `rotate`: entrance.css's keyframes write `transform: perspective(...)
2546
- // rotateX/Y(...)`, not the individual `rotate:` property — the `perspective` parameter below
2547
- // used to compile cleanly and do nothing, because nothing read it. See entrance.css's own
2548
- // comment on `kui-flip-in-x` for the fix; `CHANNEL.skew` is this catalog's name for "claims the
2549
- // whole `transform` shorthand" (`core/types.ts`), shared with `scroll-skew` and `flip-face`.
2550
- cssPrimitive("flip-3d", [CHANNEL.skew], {
2551
- timelines: ENTRANCE_TIMELINES,
2552
- parameters: {
2553
- angle: { type: "angle", default: "90deg", cssProperty: "--kui-from-angle" },
2554
- perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
2555
- }
2556
- }),
2557
- cssPrimitive("blur", [CHANNEL.filter], {
2558
- timelines: ENTRANCE_TIMELINES,
2559
- parameters: { blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
2560
- }),
2561
- // Purpose-built combination: one keyframe, so opacity is written once instead of twice.
2562
- cssPrimitive("reveal-blur", [CHANNEL.opacity, CHANNEL.translate, CHANNEL.filter], {
2563
- timelines: ENTRANCE_TIMELINES,
2564
- parameters: { ...distance, blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
2565
- }),
2566
- // --- scroll-linked ---------------------------------------------------------------------
2567
- // These are progress-linked, not time-triggered: they reverse as the user scrolls back.
2568
- // That is by design and is why `timeline:` is a different axis from `on:`.
2569
- // `distance.distance` only, not the whole `distance` object: `kui-parallax-y`/`-x` (scroll.css)
2570
- // write only `translate` — this primitive doesn't declare `CHANNEL.opacity`. Same dead-parameter
2571
- // shape as `scale-move`/`roll` above; `parallax-y`/`parallax-x`/`depth-layer` never read
2572
- // `--kui-from-opacity`.
2573
- cssPrimitive("parallax", [CHANNEL.translate], {
2574
- parameters: { distance: distance.distance },
2575
- timelines: ["view", "scroll", "pin"],
2576
- activations: ["manual"],
2577
- reducedMotion: "disable",
2578
- perfClass: "compositor"
2579
- }),
2580
- // `from` exists because the resting end was hardcoded (scale 1 / rotate 0deg), which fixed
2581
- // these to "grow slightly" and "tilt slightly" — a scroll-driven element that should sweep in
2582
- // from a quarter-size or from half a turn away had no way to say so. Same `--kui-from-*`
2583
- // properties the entrance primitives already use, so the two stay spellable the same way.
2584
- cssPrimitive("parallax-scale", [CHANNEL.scale], {
2585
- parameters: {
2586
- scale: { type: "number", default: "1.2", cssProperty: "--kui-to-scale" },
2587
- from: { type: "number", default: "1", cssProperty: "--kui-from-scale" }
2588
- },
2589
- timelines: ["view", "scroll", "pin"],
2590
- activations: ["manual"],
2591
- reducedMotion: "disable"
2592
- }),
2593
- cssPrimitive("parallax-rotate", [CHANNEL.rotate], {
2594
- parameters: {
2595
- angle: { type: "angle", default: "12deg", cssProperty: "--kui-to-angle" },
2596
- from: { type: "angle", default: "0deg", cssProperty: "--kui-from-angle" }
2597
- },
2598
- timelines: ["view", "scroll", "pin"],
2599
- activations: ["manual"],
2600
- reducedMotion: "disable"
2601
- }),
2602
- cssPrimitive("scroll-fade", [CHANNEL.opacity], {
2603
- parameters: { opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" } },
2604
- timelines: ["view", "scroll", "pin"],
2605
- activations: ["manual"],
2606
- reducedMotion: "disable"
2607
- }),
2608
- // `filter`, not `opacity` — it collides with `blur`, and declaring the real channel is what
2609
- // makes `channels.ts` say so instead of letting the two silently overwrite each other's
2610
- // `filter` declaration.
2611
- cssPrimitive("desaturate", [CHANNEL.filter], {
2612
- parameters: {
2613
- from: { type: "percentage", default: "100%", cssProperty: "--kui-from-grayscale" },
2614
- to: { type: "percentage", default: "0%", cssProperty: "--kui-to-grayscale" }
2615
- },
2616
- timelines: ["view", "scroll", "pin"],
2617
- activations: ["manual"],
2618
- reducedMotion: "disable",
2619
- perfClass: "paint"
2620
- }),
2621
- /*
2622
- * `transform`, not one of the independent transform properties, because CSS never shipped a
2623
- * standalone `skew:`. That is also why `skew` is its own channel rather than folded in with
2624
- * `rotate`: writing `transform` replaces the entire shorthand, so a skew composed with anything
2625
- * else that wrote `transform` would silently win. Nothing else in the catalog does — every other
2626
- * transform in the library goes through `translate`/`rotate`/`scale` — so the shorthand is free.
2627
- */
2628
- cssPrimitive("skew", [CHANNEL.skew], {
2629
- parameters: {
2630
- from: { type: "angle", default: "8deg", cssProperty: "--kui-from-skew" },
2631
- to: { type: "angle", default: "0deg", cssProperty: "--kui-to-skew" }
2632
- },
2633
- timelines: ["view", "scroll", "pin"],
2634
- activations: ["manual"],
2635
- reducedMotion: "disable"
2636
- }),
2637
- cssPrimitive("progress", [CHANNEL.scale], {
2638
- timelines: ["scroll", "view"],
2639
- activations: ["manual"],
2640
- reducedMotion: "disable"
2641
- }),
2642
- // Separate primitive because it writes `stroke-dashoffset`, not `scale` — declaring the
2643
- // wrong channel would let it silently compose with an effect it actually collides with.
2644
- cssPrimitive("progress-stroke", [CHANNEL.stroke], {
2645
- parameters: { length: { type: "number", default: "100", cssProperty: "--kui-path-length" } },
2646
- timelines: ["scroll", "view"],
2647
- activations: ["manual"],
2648
- reducedMotion: "disable",
2649
- perfClass: "paint"
2650
- })
2651
- ];
2652
- var p = (name, primitive, keyframes, params) => ({ name, primitive, keyframes, ...params ? { params } : {} });
2653
- var pIn = (name, primitive, keyframes, params) => ({ ...p(name, primitive, keyframes, params), cloak: true });
2654
- var FADE = [
2655
- pIn("fade-in", "reveal", "kui-in"),
2656
- p("fade-out", "reveal", "kui-out"),
2657
- pIn("fade-up", "reveal", "kui-in-up"),
2658
- pIn("fade-down", "reveal", "kui-in-down"),
2659
- pIn("fade-left", "reveal", "kui-in-left"),
2660
- pIn("fade-right", "reveal", "kui-in-right"),
2661
- p("fade-out-up", "reveal", "kui-out-up"),
2662
- p("fade-out-down", "reveal", "kui-out-down"),
2663
- p("fade-out-left", "reveal", "kui-out-left"),
2664
- p("fade-out-right", "reveal", "kui-out-right")
2665
- ];
2666
- var SLIDE_PARAMS = { distance: "100px", opacity: "1" };
2667
- var SLIDE = [
2668
- pIn("slide-up", "reveal", "kui-in-up", SLIDE_PARAMS),
2669
- pIn("slide-down", "reveal", "kui-in-down", SLIDE_PARAMS),
2670
- pIn("slide-left", "reveal", "kui-in-left", SLIDE_PARAMS),
2671
- pIn("slide-right", "reveal", "kui-in-right", SLIDE_PARAMS),
2672
- p("slide-out-up", "reveal", "kui-out-up", SLIDE_PARAMS),
2673
- p("slide-out-down", "reveal", "kui-out-down", SLIDE_PARAMS),
2674
- p("slide-out-left", "reveal", "kui-out-left", SLIDE_PARAMS),
2675
- p("slide-out-right", "reveal", "kui-out-right", SLIDE_PARAMS)
2676
- ];
2677
- var LOGICAL = [
2678
- pIn("slide-inline-start", "reveal", "kui-in-inline-start", SLIDE_PARAMS),
2679
- pIn("slide-inline-end", "reveal", "kui-in-inline-end", SLIDE_PARAMS),
2680
- pIn("slide-block-start", "reveal", "kui-in-up", SLIDE_PARAMS),
2681
- pIn("slide-block-end", "reveal", "kui-in-down", SLIDE_PARAMS)
2682
- ];
2683
- var ZOOM = [
2684
- pIn("zoom-in", "scale", "kui-zoom-in"),
2685
- p("zoom-out", "scale", "kui-zoom-out"),
2686
- pIn("pop-in", "scale", "kui-zoom-in", { scale: "0.6", ease: "back-out" }),
2687
- p("pop-out", "scale", "kui-zoom-out", { scale: "0.6", ease: "back-in" }),
2688
- pIn("zoom-in-up", "scale-move", "kui-zoom-in-up"),
2689
- pIn("zoom-in-down", "scale-move", "kui-zoom-in-down")
2690
- ];
2691
- var FLIP = [
2692
- pIn("flip-in-x", "flip-3d", "kui-flip-in-x"),
2693
- pIn("flip-in-y", "flip-3d", "kui-flip-in-y"),
2694
- p("flip-out-x", "flip-3d", "kui-flip-out-x"),
2695
- p("flip-out-y", "flip-3d", "kui-flip-out-y")
2696
- ];
2697
- var ROTATE = [
2698
- pIn("rotate-in", "rotate", "kui-rotate-in"),
2699
- p("rotate-out", "rotate", "kui-rotate-out"),
2700
- pIn("rotate-in-left", "rotate", "kui-rotate-in", { angle: "-45deg" }),
2701
- pIn("rotate-in-right", "rotate", "kui-rotate-in", { angle: "45deg" }),
2702
- pIn("roll-in", "roll", "kui-roll-in"),
2703
- p("roll-out", "roll", "kui-roll-out"),
2704
- pIn("swing-in", "rotate", "kui-swing-in", { angle: "-15deg", ease: "back-out" })
2705
- ];
2706
- var BLUR = [
2707
- pIn("blur-in", "blur", "kui-blur-in"),
2708
- p("blur-out", "blur", "kui-blur-out"),
2709
- pIn("fade-blur-up", "reveal-blur", "kui-fade-blur-up"),
2710
- pIn("fade-blur-in", "reveal-blur", "kui-fade-blur-in")
2711
- ];
2712
- var CHARACTER = [
2713
- pIn("bounce-in", "scale", "kui-zoom-in", { scale: "0.3", ease: "bounce" }),
2714
- pIn("bounce-in-up", "reveal", "kui-in-up", { distance: "60px", ease: "back-out" }),
2715
- pIn("bounce-in-down", "reveal", "kui-in-down", { distance: "60px", ease: "back-out" }),
2716
- pIn("back-in-up", "reveal", "kui-in-up", { distance: "120px", ease: "expo-out" }),
2717
- pIn("back-in-down", "reveal", "kui-in-down", { distance: "120px", ease: "expo-out" })
2718
- ];
2719
- var SCROLL = [
2720
- p("parallax-y", "parallax", "kui-parallax-y"),
2721
- p("parallax-x", "parallax", "kui-parallax-x"),
2722
- p("parallax-scale", "parallax-scale", "kui-parallax-scale"),
2723
- p("parallax-rotate", "parallax-rotate", "kui-parallax-rotate"),
2724
- p("depth-layer", "parallax", "kui-parallax-y", { distance: "200px" }),
2725
- p("scroll-fade", "scroll-fade", "kui-scroll-fade"),
2726
- p("scroll-desaturate", "desaturate", "kui-desaturate"),
2727
- p("scroll-skew", "skew", "kui-scroll-skew"),
2728
- p("scroll-progress-bar", "progress", "kui-progress-x"),
2729
- p("scroll-progress-bar-y", "progress", "kui-progress-y"),
2730
- p("scroll-progress-ring", "progress-stroke", "kui-progress-ring"),
2731
- // `reveal-repeat` was removed: it was byte-identical to `reveal-once`, and the activation
2732
- // binder unobserves after first entry, so a repeating reveal is not implementable yet.
2733
- pIn("reveal-once", "reveal", "kui-in-up")
2734
- ];
2735
- var PRESETS = [
2736
- ...FADE,
2737
- ...SLIDE,
2738
- ...LOGICAL,
2739
- ...ZOOM,
2740
- ...FLIP,
2741
- ...ROTATE,
2742
- ...BLUR,
2743
- ...CHARACTER,
2744
- ...SCROLL
2745
- ];
2746
- var COMBOS = [
2747
- [["fade-up", "blur-in"], "fade-blur-up"],
2748
- [["fade-in", "blur-in"], "fade-blur-in"]
2749
- ];
2750
- function registerCore(registry) {
2751
- registry.registerPrimitives(PRIMITIVES).registerPresets(PRESETS);
2752
- for (const [names, preset] of COMBOS) registry.registerCombo(names, preset);
2753
- return registry;
2754
- }
2755
-
2756
- // src/effects/step-marking.ts
2757
- var STEP_STATE_ATTR = "data-kui-step-state";
2758
- function selectorBreadth(selector, doc) {
2759
- try {
2760
- if (doc.documentElement.matches(selector)) return "document-wide";
2761
- if (doc.body?.matches(selector)) return "document-wide";
2762
- return "ok";
2763
- } catch {
2764
- return "invalid";
2765
- }
2766
- }
2767
- function resolveTarget(selector, ctx, effect) {
2768
- if (!selector) return selector;
2769
- const breadth = selectorBreadth(selector, ctx.doc);
2770
- if (breadth === "invalid") {
2771
- ctx.warn(`${effect} target "${selector}" is not a valid selector and will be ignored`);
2772
- return "";
2773
- }
2774
- if (breadth === "document-wide") {
2775
- ctx.warn(`${effect} target "${selector}" matches the whole document and will be ignored`);
2776
- return "";
2777
- }
2778
- return selector;
2779
- }
2780
- function createStepMarker(resolve) {
2781
- const ledgers = /* @__PURE__ */ new Map();
2782
- return {
2783
- mark(index) {
2784
- const seen = /* @__PURE__ */ new Map();
2785
- for (const node of resolve()) {
2786
- const parent = node.parentElement;
2787
- const position = seen.get(parent) ?? 0;
2788
- seen.set(parent, position + 1);
2789
- let ledger = ledgers.get(node);
2790
- if (!ledger) {
2791
- ledger = createAttributeLedger(node);
2792
- ledgers.set(node, ledger);
2793
- }
2794
- ledger.set(STEP_STATE_ATTR, stepStateFor(position, index));
2795
- }
2796
- },
2797
- restore() {
2798
- for (const ledger of ledgers.values()) ledger.restore();
2799
- ledgers.clear();
2800
- }
2801
- };
2802
- }
2803
- function stepStateFor(position, index) {
2804
- if (position < index) return "before";
2805
- if (position === index) return "active";
2806
- return "after";
2807
- }
2808
-
2809
- // src/effects/forms/primitives.ts
2810
- var timing = {
2811
- duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
2812
- ...TRIGGER_DELAY_PARAM,
2813
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
2814
- };
2815
- var NATIVE_STATE_PRIMITIVE = {
2816
- id: "native-state",
2817
- renderer: "javascript",
2818
- channels: [CHANNEL.translate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.stroke, CHANNEL.color],
2819
- parameters: {},
2820
- supportedTimelines: ["time"],
2821
- supportedActivations: ["load"],
2822
- defaultActivation: "load",
2823
- perfClass: "compositor",
2824
- // Native pseudo-classes drive these and the motion lands on a sibling, not on the control that
2825
- // carries the attribute, so base.css's policy layer enforces this through its sibling
2826
- // `transition-duration` rules rather than the `animation-*` ones.
2827
- reducedMotion: "disable",
2828
- prepare: () => inertInstance()
2829
- };
2830
- function prepareSiblingScale(cssProperty) {
2831
- return (el, params) => {
2832
- const sibling = el.nextElementSibling;
2833
- sibling?.style.setProperty(cssProperty, String(params.num("scale", 1)));
2834
- return () => sibling?.style.removeProperty(cssProperty);
2835
- };
2836
- }
2837
- function siblingScalePrimitive(id, cssProperty) {
2838
- return {
2839
- id,
2840
- renderer: "javascript",
2841
- channels: [CHANNEL.translate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.stroke, CHANNEL.color],
2842
- parameters: { scale: { type: "number", default: "1", cssProperty } },
2843
- supportedTimelines: ["time"],
2844
- supportedActivations: ["load"],
2845
- defaultActivation: "load",
2846
- perfClass: "compositor",
2847
- reducedMotion: "disable",
2848
- prepare: deferPrepare(prepareSiblingScale(cssProperty))
2849
- };
2850
- }
2851
- var TOGGLE_MORPH_PRIMITIVE = siblingScalePrimitive("toggle-morph", "--kui-toggle-scale");
2852
- var RADIO_FILL_PRIMITIVE = siblingScalePrimitive("radio-fill", "--kui-radio-scale");
2853
- var FOCUS_RING_PRIMITIVE = cssPrimitive("focus-ring", ["shadow"], {
2854
- activations: ["focus", "manual"],
2855
- defaultActivation: "focus"
2856
- });
2857
- var VALIDATE_SHAKE_PRIMITIVE = cssPrimitive("validate-shake", [CHANNEL.translate], {
2858
- activations: ["click", "manual"],
2859
- defaultActivation: "click"
2860
- });
2861
- var VALIDATE_CHECK_PRIMITIVE = cssPrimitive("validate-check", [CHANNEL.stroke], {
2862
- activations: ["click", "manual"],
2863
- defaultActivation: "click"
2864
- });
2865
- function jsInputPrimitive(id, channels, parameters, prepare) {
2866
- return {
2867
- id,
2868
- renderer: "javascript",
2869
- channels,
2870
- parameters: { ...timing, ...parameters },
2871
- supportedTimelines: ["time"],
2872
- supportedActivations: ["load"],
2873
- defaultActivation: "load",
2874
- perfClass: "compositor",
2875
- reducedMotion: "disable",
2876
- prepare
2877
- };
2878
- }
2879
- function computeStrength(value) {
2880
- let score = 0;
2881
- if (value.length >= 6) score++;
2882
- if (value.length >= 10) score++;
2883
- if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++;
2884
- if (/\d/.test(value)) score++;
2885
- if (/[^A-Za-z0-9]/.test(value)) score++;
2886
- return Math.min(4, score);
2887
- }
2888
- function prepareStrengthMeter(el) {
2889
- const input = el;
2890
- const update = () => {
2891
- el.setAttribute("data-kui-strength-level", String(computeStrength(input.value)));
2892
- };
2893
- input.addEventListener("input", update);
2894
- update();
2895
- return () => {
2896
- input.removeEventListener("input", update);
2897
- el.removeAttribute("data-kui-strength-level");
2898
- };
2899
- }
2900
- function prepareRangeFill(el, params, ctx) {
2901
- const input = el;
2902
- const update = () => {
2903
- const min = Number(input.min || "0");
2904
- const max = Number(input.max || "100");
2905
- const pct = max > min ? (Number(input.value) - min) / (max - min) * 100 : 0;
2906
- ctx.style.set("--kui-fill", `${pct.toFixed(2)}%`);
2907
- };
2908
- input.addEventListener("input", update);
2909
- update();
2910
- return () => input.removeEventListener("input", update);
2911
- }
2912
- var STRENGTH_METER_PRIMITIVE = jsInputPrimitive(
2913
- "strength-meter",
2914
- ["meter"],
2915
- {},
2916
- deferPrepare(prepareStrengthMeter)
2917
- );
2918
- var RANGE_FILL_PRIMITIVE = jsInputPrimitive(
2919
- "range-fill",
2920
- [CHANNEL.background],
2921
- {},
2922
- deferPrepare(prepareRangeFill)
2923
- );
2924
- function nextStep(step, total) {
2925
- return total > 0 ? (step + 1) % total : 0;
2926
- }
2927
- function prepareStepProgress(el, params, ctx) {
2928
- const total = Math.max(1, Math.round(params.num("steps", 4)));
2929
- const selector = resolveTarget(params.text("target"), ctx, "step-progress");
2930
- const marker = createStepMarker(
2931
- () => selector ? ctx.doc.querySelectorAll(selector) : el.children
2932
- );
2933
- const self = createAttributeLedger(el);
2934
- let step = 0;
2935
- const render = () => {
2936
- self.set("data-kui-step", String(step));
2937
- marker.mark(step);
2938
- };
2939
- const advance = () => {
2940
- step = nextStep(step, total);
2941
- render();
2942
- };
2943
- el.addEventListener("click", advance);
2944
- render();
2945
- return () => {
2946
- el.removeEventListener("click", advance);
2947
- self.restore();
2948
- marker.restore();
2949
- };
2950
- }
2951
- var STEP_PROGRESS_PRIMITIVE = jsInputPrimitive(
2952
- "step-progress",
2953
- ["state"],
2954
- {
2955
- steps: { type: "number", default: "4", cssProperty: "--kui-steps", minimum: 1, maximum: 20, integer: true },
2956
- target: { type: "text", default: "", cssProperty: "--kui-target" }
2957
- },
2958
- deferPrepare(prepareStepProgress)
2959
- );
2960
- function nextSubmitStage(stage) {
2961
- if (stage === "idle") return "loading";
2962
- if (stage === "loading") return "done";
2963
- return "idle";
2964
- }
2965
- function prepareSubmitFlow(el, params, ctx) {
2966
- const loadMs = params.ms("load", 1200);
2967
- const holdMs = params.ms("hold", 1500);
2968
- let stage = "idle";
2969
- let handle;
2970
- const render = () => el.setAttribute("data-kui-stage", stage);
2971
- const toIdle = () => {
2972
- stage = nextSubmitStage(stage);
2973
- render();
2974
- };
2975
- const toDone = () => {
2976
- stage = nextSubmitStage(stage);
2977
- render();
2978
- handle = ctx.win.setTimeout(toIdle, holdMs);
2979
- };
2980
- const advance = () => {
2981
- if (stage !== "idle") return;
2982
- stage = nextSubmitStage(stage);
2983
- render();
2984
- handle = ctx.win.setTimeout(toDone, loadMs);
2985
- };
2986
- el.addEventListener("click", advance);
2987
- render();
2988
- return () => {
2989
- el.removeEventListener("click", advance);
2990
- if (handle !== void 0) ctx.win.clearTimeout(handle);
2991
- el.removeAttribute("data-kui-stage");
2992
- };
2993
- }
2994
- var SUBMIT_FLOW_PRIMITIVE = jsInputPrimitive(
2995
- "submit-flow",
2996
- ["state"],
2997
- {
2998
- load: { type: "time", default: "1200ms", cssProperty: "--kui-load" },
2999
- hold: { type: "time", default: "1500ms", cssProperty: "--kui-hold" }
3000
- },
3001
- deferPrepare(prepareSubmitFlow)
3002
- );
3003
-
3004
- // src/effects/forms/index.ts
3005
- var FORMS_PRIMITIVES = [
3006
- NATIVE_STATE_PRIMITIVE,
3007
- FOCUS_RING_PRIMITIVE,
3008
- VALIDATE_SHAKE_PRIMITIVE,
3009
- VALIDATE_CHECK_PRIMITIVE,
3010
- STRENGTH_METER_PRIMITIVE,
3011
- TOGGLE_MORPH_PRIMITIVE,
3012
- RADIO_FILL_PRIMITIVE,
3013
- RANGE_FILL_PRIMITIVE,
3014
- STEP_PROGRESS_PRIMITIVE,
3015
- SUBMIT_FLOW_PRIMITIVE
3016
- ];
3017
- var FORMS_PRESETS = [
3018
- { name: "label-float", primitive: "native-state" },
3019
- { name: "input-underline-grow", primitive: "native-state" },
3020
- { name: "focus-ring-grow", primitive: "focus-ring", keyframes: "kui-focus-ring-grow" },
3021
- { name: "validate-shake", primitive: "validate-shake", keyframes: "kui-validate-shake" },
3022
- { name: "validate-check", primitive: "validate-check", keyframes: "kui-validate-check" },
3023
- { name: "strength-meter", primitive: "strength-meter" },
3024
- { name: "toggle-morph", primitive: "toggle-morph" },
3025
- { name: "checkbox-draw", primitive: "native-state" },
3026
- { name: "radio-fill", primitive: "radio-fill" },
3027
- { name: "range-fill", primitive: "range-fill" },
3028
- { name: "submit-to-spinner-to-check", primitive: "submit-flow" },
3029
- { name: "step-progress", primitive: "step-progress" }
3030
- ];
3031
- function registerForms(registry) {
3032
- return registry.registerPrimitives(FORMS_PRIMITIVES).registerPresets(FORMS_PRESETS);
3033
- }
3034
-
3035
- // src/core/gesture.ts
3036
- var VELOCITY_WINDOW_MS = 100;
3037
- var MAX_SAMPLES = 12;
3038
- function velocityFrom(samples) {
3039
- const last = samples[samples.length - 1];
3040
- if (!last || samples.length < 2) return { vx: 0, vy: 0 };
3041
- const cutoff = last.time - VELOCITY_WINDOW_MS;
3042
- const first = samples.find((sample) => sample.time >= cutoff) ?? samples[0];
3043
- const span = (last.time - first.time) / 1e3;
3044
- if (span <= 0) return { vx: 0, vy: 0 };
3045
- return { vx: (last.x - first.x) / span, vy: (last.y - first.y) / span };
3046
- }
3047
- function swipeDirection(vector, minVelocity) {
3048
- const { vx, vy } = vector;
3049
- if (Math.abs(vx) < minVelocity && Math.abs(vy) < minVelocity) return null;
3050
- if (Math.abs(vx) >= Math.abs(vy)) return vx > 0 ? "right" : "left";
3051
- return vy > 0 ? "down" : "up";
3052
- }
3053
- function applyAxis(vector, axis) {
3054
- if (axis === "x") return { ...vector, dy: 0, vy: 0 };
3055
- if (axis === "y") return { ...vector, dx: 0, vx: 0 };
3056
- return vector;
3057
- }
3058
- function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
3059
- const threshold = options.threshold ?? 4;
3060
- const axis = options.axis ?? "both";
3061
- const swipeVelocity = options.swipeVelocity ?? 300;
3062
- const longPressMs = options.longPressMs ?? 0;
3063
- let samples = [];
3064
- let origin = null;
3065
- let active = false;
3066
- let longPressTimer = null;
3067
- let longPressFired = false;
3068
- function sampleOf(event) {
3069
- return { x: event.clientX, y: event.clientY, time: deps.now() };
3070
- }
3071
- function vectorNow(sample) {
3072
- const start = origin;
3073
- const { vx, vy } = velocityFrom(samples);
3074
- return applyAxis({ dx: sample.x - start.x, dy: sample.y - start.y, vx, vy }, axis);
3075
- }
3076
- function clearLongPress() {
3077
- if (longPressTimer !== null) deps.clearTimer(longPressTimer);
3078
- longPressTimer = null;
3079
- }
3080
- function onDown(event) {
3081
- origin = sampleOf(event);
3082
- samples = [origin];
3083
- active = false;
3084
- longPressFired = false;
3085
- el.setPointerCapture?.(event.pointerId);
3086
- if (longPressMs > 0) {
3087
- longPressTimer = deps.setTimer(() => {
3088
- longPressFired = true;
3089
- handlers.onLongPress?.(origin);
3090
- }, longPressMs);
3091
- }
3092
- }
3093
- function onMove(event) {
3094
- if (!origin) return;
3095
- const sample = sampleOf(event);
3096
- samples.push(sample);
3097
- if (samples.length > MAX_SAMPLES) samples.shift();
3098
- const vector = vectorNow(sample);
3099
- if (!active && Math.hypot(vector.dx, vector.dy) < threshold) return;
3100
- if (!active) {
3101
- active = true;
3102
- clearLongPress();
3103
- handlers.onStart?.(origin);
3104
- }
3105
- handlers.onMove?.(vector, sample);
3106
- }
3107
- function onUp(event) {
3108
- clearLongPress();
3109
- el.releasePointerCapture?.(event.pointerId);
3110
- if (!origin) return;
3111
- const sample = sampleOf(event);
3112
- samples.push(sample);
3113
- const vector = vectorNow(sample);
3114
- if (active) {
3115
- handlers.onEnd?.(vector, sample);
3116
- const direction = swipeDirection(vector, swipeVelocity);
3117
- if (direction) handlers.onSwipe?.(direction, vector);
3118
- } else if (longPressFired) {
3119
- handlers.onEnd?.(vector, sample);
3120
- }
3121
- origin = null;
3122
- active = false;
3123
- longPressFired = false;
3124
- samples = [];
3125
- }
3126
- el.addEventListener("pointerdown", onDown);
3127
- el.addEventListener("pointermove", onMove, { passive: true });
3128
- el.addEventListener("pointerup", onUp, { passive: true });
3129
- el.addEventListener("pointercancel", onUp, { passive: true });
3130
- return () => {
3131
- clearLongPress();
3132
- el.removeEventListener("pointerdown", onDown);
3133
- el.removeEventListener("pointermove", onMove);
3134
- el.removeEventListener("pointerup", onUp);
3135
- el.removeEventListener("pointercancel", onUp);
3136
- };
3137
- }
3138
- function defaultGestureDeps() {
3139
- return {
3140
- now: () => typeof performance === "undefined" ? Date.now() : performance.now(),
3141
- setTimer: (callback, ms) => globalThis.setTimeout(callback, ms),
3142
- clearTimer: (handle) => globalThis.clearTimeout(handle)
3143
- };
3144
- }
3145
- function rubberBand(offset, limit, tension = 0.55) {
3146
- if (limit <= 0) return 0;
3147
- const sign = Math.sign(offset);
3148
- const magnitude = Math.abs(offset);
3149
- return sign * (1 - 1 / (magnitude / limit / tension + 1)) * limit;
3150
- }
3151
-
3152
- // src/effects/gestures/primitives.ts
3153
- var springParams2 = {
3154
- stiffness: {
3155
- type: "number",
3156
- default: "180",
3157
- cssProperty: "--kui-stiffness",
3158
- finite: true,
3159
- minimum: 1,
3160
- maximum: 1e4
3161
- },
3162
- damping: {
3163
- type: "number",
3164
- default: "24",
3165
- cssProperty: "--kui-damping",
3166
- finite: true,
3167
- minimum: 0.1,
3168
- maximum: 1e3
3169
- },
3170
- mass: {
3171
- type: "number",
3172
- default: "1",
3173
- cssProperty: "--kui-mass",
3174
- finite: true,
3175
- minimum: 0.1
3176
- }
3177
- };
3178
- function gesturePrimitive(id, channels, parameters, prepare) {
3179
- return {
3180
- id,
3181
- renderer: "javascript",
3182
- channels,
3183
- parameters,
3184
- supportedTimelines: ["time", "pointer"],
3185
- supportedActivations: ["load", "manual"],
3186
- defaultActivation: "load",
3187
- perfClass: "continuous",
3188
- reducedMotion: "disable",
3189
- prepare
3190
- };
3191
- }
3192
- function springFrom2(params) {
3193
- return {
3194
- ...DEFAULT_SPRING,
3195
- stiffness: params.num("stiffness", DEFAULT_SPRING.stiffness),
3196
- damping: params.num("damping", DEFAULT_SPRING.damping),
3197
- mass: params.num("mass", DEFAULT_SPRING.mass)
3198
- };
3199
- }
3200
- function springDeps(ctx) {
3201
- return { ...defaultSpringDeps(), warn: ctx.warn };
3202
- }
3203
- function writeOffset(ctx, x, y) {
3204
- ctx.style.set("translate", `${x.toFixed(2)}px ${y.toFixed(2)}px`);
3205
- }
3206
- function prepareDraggable(el, params, ctx) {
3207
- const config = springFrom2(params);
3208
- const bounds = params.num("bounds", 0);
3209
- const returns = params.is("return");
3210
- const inertia = params.is("inertia");
3211
- const resistance = params.num("resistance", 0.55);
3212
- const momentum = params.num("momentum", 0.2);
3213
- const offset = { x: 0, y: 0 };
3214
- const pickup = { x: 0, y: 0 };
3215
- const deps = springDeps(ctx);
3216
- const runners = {
3217
- x: createSpringRunner(config, (value) => write({ ...offset, x: value }), deps),
3218
- y: createSpringRunner(config, (value) => write({ ...offset, y: value }), deps)
3219
- };
3220
- function write(next) {
3221
- offset.x = next.x;
3222
- offset.y = next.y;
3223
- writeOffset(ctx, offset.x, offset.y);
3224
- }
3225
- const stopRecognising = recognise(
3226
- el,
3227
- {
3228
- onStart() {
3229
- runners.x.stop();
3230
- runners.y.stop();
3231
- pickup.x = offset.x;
3232
- pickup.y = offset.y;
3233
- el.setAttribute("data-kui-dragging", "true");
3234
- },
3235
- onMove(vector) {
3236
- write({
3237
- x: resist(pickup.x + vector.dx, bounds, resistance),
3238
- y: resist(pickup.y + vector.dy, bounds, resistance)
3239
- });
3240
- },
3241
- onEnd(vector) {
3242
- el.setAttribute("data-kui-dragging", "false");
3243
- settle(runners, offset, vector, { returns, inertia, momentum });
3244
- }
3245
- },
3246
- { axis: params.text("axis", "both") }
3247
- );
3248
- ctx.invalidate();
3249
- return () => {
3250
- stopRecognising();
3251
- runners.x.stop();
3252
- runners.y.stop();
3253
- el.removeAttribute("data-kui-dragging");
3254
- };
3255
- }
3256
- function resist(delta, bounds, tension) {
3257
- return bounds > 0 ? rubberBand(delta, bounds, tension) : delta;
3258
- }
3259
- function settle(runners, offset, vector, mode) {
3260
- runners.x.set(offset.x, mode.inertia ? vector.vx : 0);
3261
- runners.y.set(offset.y, mode.inertia ? vector.vy : 0);
3262
- if (mode.returns) {
3263
- runners.x.to(0);
3264
- runners.y.to(0);
3265
- return;
3266
- }
3267
- const carry = mode.inertia ? mode.momentum : 0;
3268
- runners.x.to(offset.x + vector.vx * carry);
3269
- runners.y.to(offset.y + vector.vy * carry);
3270
- }
3271
- function prepareSwipeable(el, params) {
3272
- const stop = recognise(
3273
- el,
3274
- {
3275
- onSwipe(direction) {
3276
- el.setAttribute("data-kui-swipe", direction);
3277
- }
3278
- },
3279
- {
3280
- axis: params.text("axis", "both"),
3281
- swipeVelocity: params.num("velocity", 300)
3282
- }
3283
- );
3284
- return () => {
3285
- stop();
3286
- el.removeAttribute("data-kui-swipe");
3287
- };
3288
- }
3289
- function preparePressable(el, params) {
3290
- const stop = recognise(
3291
- el,
3292
- {
3293
- onLongPress() {
3294
- el.setAttribute("data-kui-pressed", "true");
3295
- },
3296
- onEnd() {
3297
- el.setAttribute("data-kui-pressed", "false");
3298
- }
3299
- },
3300
- { longPressMs: effectDurationMs(params, 500) }
3301
- );
3302
- return () => {
3303
- stop();
3304
- el.removeAttribute("data-kui-pressed");
3305
- };
3306
- }
3307
- function prepareMagnetic(el, params, ctx) {
3308
- const node = el;
3309
- const radius = params.num("radius", 120);
3310
- const strength = params.num("strength", 0.35);
3311
- const config = springFrom2(params);
3312
- const deps = springDeps(ctx);
3313
- const offset = { x: 0, y: 0 };
3314
- const runnerX = createSpringRunner(config, (value) => {
3315
- offset.x = value;
3316
- writeOffset(ctx, offset.x, offset.y);
3317
- }, deps);
3318
- const runnerY = createSpringRunner(config, (value) => {
3319
- offset.y = value;
3320
- writeOffset(ctx, offset.x, offset.y);
3321
- }, deps);
3322
- const onPointerMove = (event) => {
3323
- const box = node.getBoundingClientRect();
3324
- const dx = event.clientX - (box.left + box.width / 2);
3325
- const dy = event.clientY - (box.top + box.height / 2);
3326
- const inRange = Math.hypot(dx, dy) < radius;
3327
- runnerX.to(inRange ? dx * strength : 0);
3328
- runnerY.to(inRange ? dy * strength : 0);
3329
- };
3330
- ctx.win.addEventListener("pointermove", onPointerMove, { passive: true });
3331
- return () => {
3332
- ctx.win.removeEventListener("pointermove", onPointerMove);
3333
- runnerX.stop();
3334
- runnerY.stop();
3335
- };
3336
- }
3337
- var GESTURE_PRIMITIVES = [
3338
- gesturePrimitive(
3339
- "draggable",
3340
- ["translate"],
3341
- {
3342
- ...springParams2,
3343
- axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", values: ["x", "y", "both"] },
3344
- bounds: { type: "number", default: "0", cssProperty: "--kui-bounds" },
3345
- return: {
3346
- type: "keyword",
3347
- default: "false",
3348
- cssProperty: "--kui-return",
3349
- values: ["true", "false"]
3350
- },
3351
- inertia: {
3352
- type: "keyword",
3353
- default: "false",
3354
- cssProperty: "--kui-inertia",
3355
- values: ["true", "false"]
3356
- },
3357
- resistance: {
3358
- type: "number",
3359
- default: "0.55",
3360
- cssProperty: "--kui-resistance",
3361
- finite: true,
3362
- minimum: 0,
3363
- maximum: 1
3364
- },
3365
- momentum: { type: "number", default: "0.2", cssProperty: "--kui-momentum", finite: true }
3366
- },
3367
- deferPrepare(prepareDraggable)
3368
- ),
3369
- gesturePrimitive(
3370
- "swipeable",
3371
- ["state"],
3372
- {
3373
- axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", values: ["x", "y", "both"] },
3374
- velocity: { type: "number", default: "300", cssProperty: "--kui-velocity" }
3375
- },
3376
- deferPrepare(prepareSwipeable)
3377
- ),
3378
- gesturePrimitive(
3379
- "pressable",
3380
- ["state"],
3381
- { duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" } },
3382
- deferPrepare(preparePressable)
3383
- ),
3384
- gesturePrimitive(
3385
- "magnetic",
3386
- ["translate"],
3387
- {
3388
- ...springParams2,
3389
- radius: { type: "number", default: "120", cssProperty: "--kui-radius" },
3390
- strength: { type: "number", default: "0.35", cssProperty: "--kui-strength" }
3391
- },
3392
- deferPrepare(prepareMagnetic)
3393
- )
3394
- ];
3395
-
3396
- // src/effects/gestures/index.ts
3397
- var GESTURE_PRESETS = [
3398
- { name: "drag", primitive: "draggable" },
3399
- { name: "drag-x", primitive: "draggable", params: { axis: "x" } },
3400
- { name: "drag-y", primitive: "draggable", params: { axis: "y" } },
3401
- { name: "drag-inertia", primitive: "draggable", params: { inertia: "true" } },
3402
- { name: "throwable", primitive: "draggable", params: { inertia: "true", damping: "18" } },
3403
- { name: "elastic-pull", primitive: "draggable", params: { return: "true", bounds: "80" } },
3404
- { name: "rubber-band", primitive: "draggable", params: { return: "true", bounds: "120" } },
3405
- { name: "snap-back", primitive: "draggable", params: { return: "true", stiffness: "260" } },
3406
- { name: "swipe", primitive: "swipeable" },
3407
- { name: "swipe-x", primitive: "swipeable", params: { axis: "x" } },
3408
- { name: "long-press", primitive: "pressable" },
3409
- { name: "magnetic", primitive: "magnetic" },
3410
- { name: "magnetic-snap", primitive: "magnetic", params: { strength: "0.6", radius: "160" } }
3411
- ];
3412
- function registerGestures(registry) {
3413
- return registry.registerPrimitives(GESTURE_PRIMITIVES).registerPresets(GESTURE_PRESETS);
3414
- }
3415
-
3416
- // src/core/flip.ts
3417
- var EPSILON = 0.5;
3418
- function domMeasure(el) {
3419
- const rect = el.getBoundingClientRect();
3420
- return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
3421
- }
3422
- function domAnimate(el, keyframes, options) {
3423
- const animate = el.animate;
3424
- return typeof animate === "function" ? animate.call(el, keyframes, options) : null;
3425
- }
3426
- function createFlipEngine(deps = {}) {
3427
- const measure = deps.measure ?? domMeasure;
3428
- const animate = deps.animate ?? domAnimate;
3429
- return {
3430
- snapshot(elements) {
3431
- const boxes = /* @__PURE__ */ new Map();
3432
- for (const el of elements) boxes.set(el, measure(el));
3433
- return { boxes };
3434
- },
3435
- play(before, elements, options = {}) {
3436
- const deltas = collectDeltas(before, elements, measure, options.scale ?? false);
3437
- if (deltas.length === 0) {
3438
- return { moved: [], finished: Promise.resolve(), cancel: () => {
3439
- } };
3440
- }
3441
- return runDeltas(deltas, animate, options);
3442
- }
3443
- };
3444
- }
3445
- function collectDeltas(before, elements, measure, scale) {
3446
- const deltas = [];
3447
- for (const el of elements) {
3448
- const first = before.boxes.get(el);
3449
- if (!first) continue;
3450
- const last = measure(el);
3451
- const delta = deltaFor(el, first, last, scale);
3452
- if (delta) deltas.push(delta);
3453
- }
3454
- return deltas;
3455
- }
3456
- function deltaFor(el, first, last, scale) {
3457
- const dx = first.x - last.x;
3458
- const dy = first.y - last.y;
3459
- const sx = scale && last.width > 0 ? first.width / last.width : 1;
3460
- const sy = scale && last.height > 0 ? first.height / last.height : 1;
3461
- const still = Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON && Math.abs(sx - 1) < 1e-3 && Math.abs(sy - 1) < 1e-3;
3462
- return still ? null : { el, dx, dy, sx, sy };
3463
- }
3464
- function runDeltas(deltas, animate, options) {
3465
- const duration = options.durationMs ?? 400;
3466
- const easing = options.easing ?? "cubic-bezier(0.2, 0, 0, 1)";
3467
- const animations = [];
3468
- for (const { el, dx, dy, sx, sy } of deltas) {
3469
- const animation = animate(
3470
- el,
3471
- [
3472
- { translate: `${dx}px ${dy}px`, scale: `${sx} ${sy}` },
3473
- { translate: "0px 0px", scale: "1 1" }
3474
- ],
3475
- { duration, easing, fill: "none" }
3476
- );
3477
- if (animation) animations.push(animation);
3478
- }
3479
- const finished = Promise.all(
3480
- animations.map((animation) => animation.finished.catch(() => void 0))
3481
- ).then(() => void 0);
3482
- return {
3483
- moved: deltas.map((delta) => delta.el),
3484
- finished,
3485
- cancel() {
3486
- for (const animation of animations) animation.cancel();
3487
- }
3488
- };
3489
- }
3490
- function observeLayout(container, engine, options, observe) {
3491
- let before = engine.snapshot(container.children);
3492
- const cleanup = observe(() => {
3493
- engine.play(before, container.children, options);
3494
- before = engine.snapshot(container.children);
3495
- });
3496
- return cleanup;
3497
- }
3498
- function mutationWatcher(container) {
3499
- return (callback) => {
3500
- if (typeof MutationObserver === "undefined") return () => {
3501
- };
3502
- const observer = new MutationObserver(callback);
3503
- observer.observe(container, {
3504
- childList: true,
3505
- subtree: true,
3506
- attributes: true,
3507
- attributeFilter: ["hidden"]
3508
- });
3509
- return () => observer.disconnect();
3510
- };
3511
- }
3512
-
3513
- // src/effects/layout/primitives.ts
3514
- var timing2 = {
3515
- duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
3516
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
3517
- };
3518
- function layoutPrimitive(id, channels, parameters, prepare) {
3519
- return {
3520
- id,
3521
- renderer: "javascript",
3522
- channels,
3523
- parameters: { ...timing2, ...parameters },
3524
- supportedTimelines: ["time"],
3525
- supportedActivations: ["load", "manual", "click"],
3526
- defaultActivation: "load",
3527
- perfClass: "layout",
3528
- // A layout transition that is merely faster is still a layout transition; under reduced
3529
- // motion the correct behaviour is for elements to appear in place.
3530
- reducedMotion: "disable",
3531
- prepare
3532
- };
3533
- }
3534
- function prepareFlipContainer(el, params) {
3535
- const engine = createFlipEngine();
3536
- return observeLayout(
3537
- el,
3538
- engine,
3539
- {
3540
- durationMs: effectDurationMs(params, 400),
3541
- easing: params.text("ease"),
3542
- scale: params.is("scale")
3543
- },
3544
- mutationWatcher(el)
3545
- );
3546
- }
3547
- function prepareAutoHeight(el, params, ctx) {
3548
- const node = el;
3549
- ctx.style.set("overflow", "hidden");
3550
- ctx.style.claim("height");
3551
- const duration = effectDurationMs(params, 400);
3552
- let animation = null;
3553
- let previous = node.getBoundingClientRect().height;
3554
- const observer = watchAttribute(node, params.text("attribute"), () => {
3555
- animation?.cancel();
3556
- const endpoints = heightEndpoints(node, previous);
3557
- previous = endpoints.to;
3558
- animation = animateHeight(node, endpoints, duration, params.text("ease"));
3559
- });
3560
- ctx.invalidate();
3561
- return () => {
3562
- observer();
3563
- animation?.cancel();
3564
- };
3565
- }
3566
- function heightEndpoints(node, previous) {
3567
- node.style.removeProperty("height");
3568
- const to = node.getBoundingClientRect().height;
3569
- return { from: previous, to };
3570
- }
3571
- function animateHeight(node, endpoints, duration, easing) {
3572
- const animate = node.animate;
3573
- if (typeof animate !== "function") return null;
3574
- return animate.call(
3575
- node,
3576
- [{ height: `${endpoints.from}px` }, { height: `${endpoints.to}px` }],
3577
- { duration, easing, fill: "none" }
3578
- );
3579
- }
3580
- function prepareIndicator(el, params, ctx) {
3581
- const node = el;
3582
- const engine = createFlipEngine();
3583
- const selector = params.text("follow");
3584
- let currentShift = 0;
3585
- const move = () => {
3586
- if (!selector) return;
3587
- const target = ctx.doc.querySelector(selector);
3588
- if (!(target instanceof Element)) return;
3589
- const before = engine.snapshot([node]);
3590
- const box = target.getBoundingClientRect();
3591
- const current = node.getBoundingClientRect();
3592
- const shift = box.left - current.left + currentShift;
3593
- currentShift = shift;
3594
- ctx.style.set("width", `${box.width}px`);
3595
- ctx.style.set("translate", `${shift}px 0`);
3596
- engine.play(before, [node], {
3597
- durationMs: effectDurationMs(params, 400),
3598
- easing: params.text("ease"),
3599
- scale: true
3600
- });
3601
- };
3602
- move();
3603
- return watchAttribute(ctx.doc.documentElement, params.text("attribute"), move);
3604
- }
3605
- function watchAttribute(root, attribute, onChange) {
3606
- if (typeof MutationObserver === "undefined" || !attribute) return () => {
3607
- };
3608
- const observer = new MutationObserver(onChange);
3609
- observer.observe(root, { subtree: true, attributes: true, attributeFilter: [attribute] });
3610
- return () => observer.disconnect();
3611
- }
3612
- var LAYOUT_PRIMITIVES = [
3613
- layoutPrimitive(
3614
- "flip-container",
3615
- ["translate", "scale"],
3616
- {
3617
- scale: {
3618
- type: "keyword",
3619
- default: "false",
3620
- cssProperty: "--kui-flip-scale",
3621
- values: ["true", "false"]
3622
- }
3623
- },
3624
- deferPrepare(prepareFlipContainer)
3625
- ),
3626
- layoutPrimitive(
3627
- "auto-height",
3628
- ["layout"],
3629
- { attribute: { type: "text", default: "data-open", cssProperty: "--kui-attribute" } },
3630
- deferPrepare(prepareAutoHeight)
3631
- ),
3632
- layoutPrimitive(
3633
- "flip-indicator",
3634
- ["translate", "layout"],
3635
- {
3636
- follow: { type: "text", default: "", cssProperty: "--kui-follow" },
3637
- attribute: { type: "text", default: "aria-selected", cssProperty: "--kui-attribute" }
3638
- },
3639
- deferPrepare(prepareIndicator)
3640
- )
3641
- ];
3642
-
3643
- // src/effects/layout/presets.ts
3644
- var LAYOUT_PRESETS = [
3645
- { name: "flip-reorder", primitive: "flip-container" },
3646
- { name: "flip-filter", primitive: "flip-container" },
3647
- { name: "flip-sort", primitive: "flip-container" },
3648
- { name: "flip-shuffle", primitive: "flip-container" },
3649
- // Cards changing aspect between layouts need their size interpolated, not just their position.
3650
- { name: "grid-to-list", primitive: "flip-container", params: { scale: "true" } },
3651
- { name: "masonry-reflow", primitive: "flip-container" },
3652
- { name: "expand-to-modal", primitive: "flip-container", params: { scale: "true", duration: "500ms" } },
3653
- { name: "accordion-height", primitive: "auto-height" },
3654
- { name: "tab-indicator-slide", primitive: "flip-indicator", params: { duration: "300ms" } }
3655
- ];
3656
-
3657
- // src/effects/layout/index.ts
3658
- function registerLayout(registry) {
3659
- return registry.registerPrimitives(LAYOUT_PRIMITIVES).registerPresets(LAYOUT_PRESETS);
3660
- }
3661
-
3662
- // src/effects/navigation/index.ts
3663
- var NAV_CSS_PRIMITIVES = [
3664
- cssPrimitive("nav-reveal", [CHANNEL.opacity, CHANNEL.translate]),
3665
- cssPrimitive("menu-fullscreen", [CHANNEL.clip, CHANNEL.opacity]),
3666
- cssPrimitive("panel-reveal", [CHANNEL.opacity, CHANNEL.translate]),
3667
- cssPrimitive("drawer-slide", [CHANNEL.translate], {
3668
- defaultActivation: "click"
3669
- })
3670
- ];
3671
- var NAV_CSS_PRESETS = [
3672
- { name: "menu-stagger-open", primitive: "nav-reveal", keyframes: "kui-nav-reveal" },
3673
- { name: "menu-fullscreen", primitive: "menu-fullscreen", keyframes: "kui-menu-fullscreen" },
3674
- { name: "dropdown-open", primitive: "panel-reveal", keyframes: "kui-panel-reveal" },
3675
- {
3676
- name: "mega-menu-drop",
3677
- primitive: "panel-reveal",
3678
- keyframes: "kui-panel-reveal",
3679
- params: { duration: "550ms" }
3680
- },
3681
- {
3682
- name: "drawer-slide",
3683
- primitive: "drawer-slide",
3684
- keyframes: "kui-drawer-slide-right"
3685
- }
3686
- ];
3687
- function navPrimitive(id, channels, parameters, prepare) {
3688
- return {
3689
- id,
3690
- renderer: "javascript",
3691
- channels,
3692
- parameters,
3693
- supportedTimelines: ["time"],
3694
- supportedActivations: ["load"],
3695
- defaultActivation: "load",
3696
- perfClass: "compositor",
3697
- // A scroll-position reaction, like the scroll-mechanics category: shortening its "duration"
3698
- // is meaningless because the position, not a clock, drives it.
3699
- reducedMotion: "disable",
3700
- prepare
3701
- };
3702
- }
3703
- function subscribeScrollTop(el, ctx, onScrollTop) {
3704
- return ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => onScrollTop(frame.metrics.scrollTop));
3705
- }
3706
- function prepareHeaderShrink(el, params, ctx) {
3707
- const offset = params.num("offset", 120);
3708
- const state = createAttributeLedger(el);
3709
- const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
3710
- const progress = offset > 0 ? Math.min(1, Math.max(0, top / offset)) : 1;
3711
- ctx.style.set("--kui-shrink", progress.toFixed(4));
3712
- state.set("data-kui-shrunk", String(progress >= 1));
3713
- });
3714
- return () => {
3715
- unsubscribe();
3716
- state.restore();
3717
- };
3718
- }
3719
- function prepareHeaderHide(el, params, ctx) {
3720
- const minDelta = params.num("offset", 8);
3721
- let last = 0;
3722
- const state = createAttributeLedger(el);
3723
- const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
3724
- const delta = top - last;
3725
- if (Math.abs(delta) < minDelta) return;
3726
- state.set("data-kui-hidden", String(delta > 0 && top > minDelta));
3727
- last = top;
3728
- });
3729
- return () => {
3730
- unsubscribe();
3731
- state.restore();
3732
- };
3733
- }
3734
- function prepareBackToTop(el, params, ctx) {
3735
- const offset = params.num("offset", 400);
3736
- const state = createAttributeLedger(el);
3737
- const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
3738
- state.set("data-kui-visible", String(top > offset));
3739
- });
3740
- return () => {
3741
- unsubscribe();
3742
- state.restore();
3743
- };
3744
- }
3745
- var NAV_JS_PRIMITIVES = [
3746
- navPrimitive(
3747
- "header-shrink",
3748
- ["layout"],
3749
- { offset: { type: "number", default: "120", cssProperty: "--kui-offset" } },
3750
- deferPrepare(prepareHeaderShrink)
3751
- ),
3752
- navPrimitive(
3753
- "header-hide-on-scroll",
3754
- [CHANNEL.translate],
3755
- { offset: { type: "number", default: "8", cssProperty: "--kui-offset" } },
3756
- deferPrepare(prepareHeaderHide)
3757
- ),
3758
- navPrimitive(
3759
- "back-to-top-fade",
3760
- [CHANNEL.opacity, CHANNEL.translate],
3761
- { offset: { type: "number", default: "400", cssProperty: "--kui-offset" } },
3762
- deferPrepare(prepareBackToTop)
3763
- )
3764
- ];
3765
- var NAV_JS_PRESETS = [
3766
- { name: "header-shrink", primitive: "header-shrink" },
3767
- { name: "header-hide-on-scroll", primitive: "header-hide-on-scroll" },
3768
- { name: "back-to-top-fade", primitive: "back-to-top-fade" }
3769
- ];
3770
- var NAVIGATION_PRIMITIVES = [...NAV_CSS_PRIMITIVES, ...NAV_JS_PRIMITIVES];
3771
- var NAVIGATION_PRESETS = [...NAV_CSS_PRESETS, ...NAV_JS_PRESETS];
3772
- function registerNavigation(registry) {
3773
- return registry.registerPrimitives(NAVIGATION_PRIMITIVES).registerPresets(NAVIGATION_PRESETS);
3774
- }
3775
-
3776
- // src/effects/scroll-mechanics/tracker.ts
3777
- var domGeometry = (el) => {
3778
- const rect = el.getBoundingClientRect();
3779
- return { top: rect.top, height: rect.height };
3780
- };
3781
- var domPosition = (el) => {
3782
- const view = el.ownerDocument?.defaultView;
3783
- return view ? view.getComputedStyle(el).position : "static";
3784
- };
3785
- var domOffsetTop = (el) => {
3786
- const view = el.ownerDocument?.defaultView;
3787
- if (!view) return 0;
3788
- const parsed = Number.parseFloat(view.getComputedStyle(el).top);
3789
- return Number.isFinite(parsed) ? parsed : 0;
3790
- };
3791
- function geometrySource(el, positionOf) {
3792
- let outermostSticky = null;
3793
- for (let node = el; node; node = node.parentElement) {
3794
- if (positionOf(node) === "sticky") outermostSticky = node;
3795
- }
3796
- return outermostSticky?.parentElement ?? el;
3797
- }
3798
- function sourceTop(el, box, measure, positionOf) {
3799
- const source = geometrySource(el, positionOf);
3800
- return source === el ? box.top : measure(source).top;
3801
- }
3802
- function trackProgress(el, ctx, options, onProgress) {
3803
- const measure = options.measure ?? domGeometry;
3804
- const positionOf = options.positionOf ?? domPosition;
3805
- const offsetOf = options.offsetOf ?? domOffsetTop;
3806
- let scrollTop = 0;
3807
- let scrollportTop = 0;
3808
- const geometry2 = createMeasureCache(() => {
3809
- const box = measure(el);
3810
- const top = options.contentAnchor ? measure(options.contentAnchor).top - box.height : sourceTop(el, box, measure, positionOf);
3811
- const stickyOffset = options.stickyEl ? offsetOf(options.stickyEl) : 0;
3812
- return { contentTop: top - stickyOffset - scrollportTop + scrollTop, height: box.height };
3813
- });
3814
- return ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => {
3815
- scrollTop = frame.metrics.scrollTop;
3816
- scrollportTop = frame.metrics.viewportTop;
3817
- const box = geometry2.read(frame.epoch);
3818
- const span = resolveDistance(options.distance, { top: 0, height: box.height }, frame);
3819
- onProgress(progressFrom(box.contentTop - scrollTop, span), frame);
3820
- });
3821
- }
3822
- function resolveDistance(distance2, box, frame) {
3823
- if (!distance2) return box.height;
3824
- const basis = {
3825
- viewportWidth: frame.metrics.viewportWidth,
3826
- viewportHeight: frame.metrics.viewportHeight,
3827
- percentBasis: box.height,
3828
- fontSize: 16,
3829
- rootFontSize: 16
3830
- };
3831
- return toPixels(distance2, basis, box.height);
3832
- }
3833
- function progressFrom(top, span) {
3834
- if (span <= 0) return 0;
3835
- return clamp01(-top / span);
3836
- }
3837
-
3838
- // src/effects/scroll-mechanics/scroll-spy.ts
3839
- function prepareScrollSpy(el, params, ctx) {
3840
- const sectionsAuthored = params.text("sections");
3841
- if (sectionsAuthored) {
3842
- const sectionsSelector = resolveTarget(sectionsAuthored, ctx, "scroll-spy sections");
3843
- return prepareScrollSpyContainer(el, params, ctx, sectionsSelector);
3844
- }
3845
- return prepareScrollSpySingle(el, params, ctx);
3846
- }
3847
- function prepareScrollSpySingle(el, params, ctx) {
3848
- if (params.text("offset-top", "0px") !== "0px") {
3849
- ctx.warn('scroll-spy "offset-top" has no effect without sections: and is ignored here');
3850
- }
3851
- const selector = resolveTarget(params.text("target"), ctx, "scroll-spy");
3852
- const links = /* @__PURE__ */ new Map();
3853
- const self = createAttributeLedger(el);
3854
- let last;
3855
- const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
3856
- const active = progress > 0 && progress < 1;
3857
- if (active === last) return;
3858
- last = active;
3859
- self.set("data-kui-active", String(active));
3860
- if (selector) markLinks(ctx.doc, selector, active, links);
3861
- });
3862
- return continuousSetup(() => {
3863
- untrack();
3864
- self.restore();
3865
- for (const ledger of links.values()) ledger.restore();
3866
- });
3867
- }
3868
- function markLinks(doc, selector, active, links) {
3869
- for (const link of doc.querySelectorAll(selector)) {
3870
- let ledger = links.get(link);
3871
- if (!ledger) {
3872
- ledger = createAttributeLedger(link);
3873
- links.set(link, ledger);
3874
- }
3875
- ledger.set("data-kui-active", String(active));
3876
- }
3877
- }
3878
- function hrefHash(link) {
3879
- const href = link.getAttribute("href") ?? "";
3880
- const at = href.indexOf("#");
3881
- return at === -1 ? "" : href.slice(at);
3882
- }
3883
- function pairSectionsWithLinks(sections, links, ctx, sectionsSelector) {
3884
- const linksByHash = /* @__PURE__ */ new Map();
3885
- for (const link of links) {
3886
- const hash = hrefHash(link);
3887
- if (hash) linksByHash.set(hash, link);
3888
- }
3889
- const claimed = /* @__PURE__ */ new Set();
3890
- const pairs = sections.map((section) => {
3891
- if (!section.id) {
3892
- ctx.warn(
3893
- `scroll-spy: a section matched by sections:"${sectionsSelector}" has no id and cannot be paired with a link`
3894
- );
3895
- return { section, link: null };
3896
- }
3897
- const hash = `#${section.id}`;
3898
- const link = linksByHash.get(hash) ?? null;
3899
- if (link) claimed.add(hash);
3900
- return { section, link };
3901
- });
3902
- for (const [hash, link] of linksByHash) {
3903
- if (!claimed.has(hash)) {
3904
- ctx.warn(`scroll-spy: link "${link.getAttribute("href")}" matches no section id in sections:"${sectionsSelector}"`);
3905
- }
3906
- }
3907
- return pairs;
3908
- }
3909
- function offsetTopPixels(authored, frame) {
3910
- return toPixels(
3911
- authored,
3912
- {
3913
- viewportWidth: frame.metrics.viewportWidth,
3914
- viewportHeight: frame.metrics.viewportHeight,
3915
- percentBasis: 0,
3916
- fontSize: 16,
3917
- rootFontSize: 16
3918
- },
3919
- 0
3920
- );
3921
- }
3922
- function highestReachedIndex(tops, scrollTop, line) {
3923
- let index = -1;
3924
- for (let i = 0; i < tops.length; i++) {
3925
- if (tops[i] - scrollTop - line <= 0) index = i;
3926
- }
3927
- return index;
3928
- }
3929
- function prepareScrollSpyContainer(el, params, ctx, sectionsSelector) {
3930
- if (params.text("distance", "100vh") !== "100vh") {
3931
- ctx.warn('scroll-spy "distance" has no effect with sections: \u2014 each section measures its own height');
3932
- }
3933
- const linksSelector = resolveTarget(params.text("target"), ctx, "scroll-spy target");
3934
- const sections = sectionsSelector ? [...el.querySelectorAll(sectionsSelector)] : [];
3935
- if (sectionsSelector && sections.length === 0) {
3936
- ctx.warn(`scroll-spy sections:"${sectionsSelector}" matched nothing inside this element`);
3937
- }
3938
- const links = linksSelector ? [...el.querySelectorAll(linksSelector)] : [];
3939
- const pairs = pairSectionsWithLinks(sections, links, ctx, sectionsSelector);
3940
- const sectionLedgers = pairs.map((pair) => createAttributeLedger(pair.section));
3941
- const linkLedgers = /* @__PURE__ */ new Map();
3942
- for (const { link } of pairs) {
3943
- if (link && !linkLedgers.has(link)) linkLedgers.set(link, createAttributeLedger(link));
3944
- }
3945
- const offsetAuthored = params.text("offset-top", "0px");
3946
- let scrollTop = 0;
3947
- let scrollportTop = 0;
3948
- const contentTops = createMeasureCache(
3949
- () => pairs.map(({ section }) => domGeometry(section).top - scrollportTop + scrollTop)
3950
- );
3951
- let active = -1;
3952
- function setActive(index, value) {
3953
- const pair = pairs[index];
3954
- sectionLedgers[index].set("data-kui-active", String(value));
3955
- if (pair.link) linkLedgers.get(pair.link).set("data-kui-active", String(value));
3956
- }
3957
- const untrack = ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => {
3958
- scrollTop = frame.metrics.scrollTop;
3959
- scrollportTop = frame.metrics.viewportTop;
3960
- const tops = contentTops.read(frame.epoch);
3961
- const line = offsetTopPixels(offsetAuthored, frame);
3962
- const next = highestReachedIndex(tops, scrollTop, line);
3963
- if (next === active) return;
3964
- if (active !== -1) setActive(active, false);
3965
- if (next !== -1) setActive(next, true);
3966
- active = next;
3967
- });
3968
- return continuousSetup(() => {
3969
- untrack();
3970
- for (const ledger of sectionLedgers) ledger.restore();
3971
- for (const ledger of linkLedgers.values()) ledger.restore();
3972
- });
3973
- }
3974
-
3975
- // src/effects/scroll-mechanics/primitives.ts
3976
- var PROGRESS_VAR = "--kui-progress";
3977
- var distanceParam = {
3978
- distance: { type: "length", default: "100vh", cssProperty: "--kui-distance" }
3979
- };
3980
- var stickyParams = {
3981
- "offset-top": {
3982
- type: "length",
3983
- default: "var(--kui-pin-offset, 0px)",
3984
- cssProperty: "--kui-offset-top"
3985
- },
3986
- spacer: {
3987
- type: "keyword",
3988
- default: "false",
3989
- cssProperty: "--kui-spacer",
3990
- values: ["true", "false"]
3991
- }
3992
- };
3993
- function installSticky(node, params, ctx) {
3994
- ctx.style.set("position", "sticky");
3995
- ctx.style.set("top", params.text("offset-top", "var(--kui-pin-offset, 0px)"));
3996
- const inserted = params.is("spacer") ? insertSpacer(node, params.text("distance"), ctx) : null;
3997
- return { spacer: inserted?.spacer ?? null, dispose: () => inserted?.remove() };
3998
- }
3999
- function scrollPrimitive(spec) {
4000
- const { id, channels, parameters, prepare, perfClass = "compositor" } = spec;
4001
- return {
4002
- id,
4003
- renderer: "javascript",
4004
- channels,
4005
- parameters,
4006
- // Accepted, never read: these primitives read scroll position themselves and are never driven
4007
- // by an `animation-timeline`. The list exists so that composing the driver with the effects it
4008
- // drives — `data-kui="pin-section distance:200vh, parallax-rotate ... "` plus `timeline:pin` —
4009
- // survives `compile.ts`'s `intersect`. Without it the intersection empties, `style-plan.ts`
4010
- // refuses the timeline, and the scrub silently degrades to a one-shot. See `TIMELINE_AGNOSTIC`
4011
- // (`effects/shared.ts`) for why the name says abstention rather than support.
4012
- supportedTimelines: TIMELINE_AGNOSTIC,
4013
- supportedActivations: ["manual", "load", "enter"],
4014
- defaultActivation: "load",
4015
- perfClass,
4016
- reducedMotion: "disable",
4017
- prepare
4018
- };
4019
- }
4020
- function writeProgress(ctx, progress) {
4021
- ctx.style.set(PROGRESS_VAR, progress.toFixed(4));
4022
- }
4023
- function preparePin(el, params, ctx) {
4024
- const node = el;
4025
- const { dispose: unstick } = installSticky(node, params, ctx);
4026
- const tracked = node.parentElement ?? el;
4027
- const untrack = trackProgress(tracked, ctx, { distance: params.text("distance"), stickyEl: node }, (progress) => {
4028
- writeProgress(ctx, progress);
4029
- el.setAttribute("data-kui-pinned", progress > 0 && progress < 1 ? "true" : "false");
4030
- });
4031
- return continuousSetup(() => {
4032
- untrack();
4033
- unstick();
4034
- el.removeAttribute("data-kui-pinned");
4035
- });
4036
- }
4037
- function insertSpacer(node, distance2, ctx) {
4038
- const spacer = ctx.doc.createElement("div");
4039
- spacer.setAttribute("data-kui-spacer", "");
4040
- spacer.setAttribute("aria-hidden", "true");
4041
- spacer.style.height = distance2;
4042
- spacer.style.pointerEvents = "none";
4043
- node.after(spacer);
4044
- ctx.invalidate();
4045
- return {
4046
- spacer,
4047
- remove: () => {
4048
- spacer.remove();
4049
- ctx.invalidate();
4050
- }
4051
- };
4052
- }
4053
- function prepareProgress(el, params, ctx) {
4054
- const steps = Math.max(0, Math.round(params.num("steps", 0)));
4055
- const selector = resolveTarget(params.text("target"), ctx, "scrollytelling-step");
4056
- const marker = createStepMarker(() => ctx.doc.querySelectorAll(selector));
4057
- const self = createAttributeLedger(el);
4058
- let lastIndex;
4059
- const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
4060
- writeProgress(ctx, progress);
4061
- if (steps === 0) return;
4062
- const index = Math.min(steps - 1, Math.floor(progress * steps));
4063
- if (index === lastIndex) return;
4064
- lastIndex = index;
4065
- self.set("data-kui-step", String(index));
4066
- ctx.style.set("--kui-step", String(index));
4067
- if (selector) marker.mark(index);
4068
- });
4069
- return continuousSetup(() => {
4070
- untrack();
4071
- self.restore();
4072
- marker.restore();
4073
- });
4074
- }
4075
- function prepareHorizontal(el, params, ctx) {
4076
- const selector = resolveTarget(params.text("target"), ctx, "horizontal-scroll");
4077
- if (!selector) return prepareBareTrack(el, params, ctx);
4078
- const track = el.querySelector(selector);
4079
- if (!track) {
4080
- ctx.warn(`horizontal-scroll target "${selector}" matched nothing inside this element`);
4081
- return () => {
4082
- };
4083
- }
4084
- return prepareManagedTrack(el, track, params, ctx);
4085
- }
4086
- function prepareBareTrack(node, params, ctx) {
4087
- const authored = params.text("travel", "auto");
4088
- const travel = createMeasureCache(() => trackTravel(node, authored, node.ownerDocument));
4089
- return continuousSetup(
4090
- trackProgress(node, ctx, { distance: params.text("distance") }, (progress, frame) => {
4091
- ctx.style.set("translate", `${-progress * travel.read(frame.epoch)}px 0`);
4092
- writeProgress(ctx, progress);
4093
- })
4094
- );
4095
- }
4096
- function prepareManagedTrack(host, track, params, ctx) {
4097
- const offsetTop = params.text("offset-top", "var(--kui-pin-offset, 0px)");
4098
- ctx.style.set("position", "sticky");
4099
- ctx.style.set("top", offsetTop);
4100
- ctx.style.set("height", `calc(100vh - ${offsetTop})`);
4101
- ctx.style.set("overflow", "hidden");
4102
- ctx.style.set("display", "grid");
4103
- ctx.style.set("align-content", "center");
4104
- const { remove: removeSpacer } = insertSpacer(host, params.text("distance"), ctx);
4105
- const rail = createStyleLedger(track);
4106
- rail.set("display", "flex");
4107
- rail.set("width", "max-content");
4108
- const authored = params.text("travel", "auto");
4109
- const travel = createMeasureCache(() => trackTravel(track, authored, track.ownerDocument));
4110
- const tracked = host.parentElement ?? host;
4111
- const untrack = trackProgress(tracked, ctx, { distance: params.text("distance"), stickyEl: host }, (progress, frame) => {
4112
- rail.set("translate", `${-progress * travel.read(frame.epoch)}px 0`);
4113
- writeProgress(ctx, progress);
4114
- });
4115
- return continuousSetup(() => {
4116
- untrack();
4117
- rail.restore();
4118
- removeSpacer();
4119
- });
4120
- }
4121
- function trackTravel(node, authored, doc) {
4122
- if (authored && authored !== "auto") return toPixels(authored, ABSOLUTE_BASIS, 0);
4123
- const selfOverflow = node.scrollWidth - node.clientWidth;
4124
- if (selfOverflow > 0) return selfOverflow;
4125
- const viewportWidth = node.parentElement?.clientWidth || doc.documentElement.clientWidth;
4126
- return Math.max(0, node.scrollWidth - viewportWidth);
4127
- }
4128
- function prepareMediaScrub(el, params, ctx) {
4129
- const managed = params.is("spacer") ? installSticky(el, params, ctx) : null;
4130
- const contentAnchor = managed?.spacer ?? void 0;
4131
- const stickyEl = managed ? el : void 0;
4132
- const selector = resolveTarget(params.text("target"), ctx, "media-scrub");
4133
- const scrub = selector ? prepareTargetScrub(el, params, ctx, { selector, contentAnchor, stickyEl }) : prepareSrcScrub(el, params, ctx, { contentAnchor, stickyEl });
4134
- return continuousSetup(() => {
4135
- scrub();
4136
- managed?.dispose();
4137
- });
4138
- }
4139
- function prepareTargetScrub(el, params, ctx, authored) {
4140
- const { selector, contentAnchor, stickyEl } = authored;
4141
- const marker = createStepMarker(() => ctx.doc.querySelectorAll(selector));
4142
- const frames = Math.max(1, ctx.doc.querySelectorAll(selector).length);
4143
- let lastIndex;
4144
- const untrack = trackProgress(el, ctx, { distance: params.text("distance"), contentAnchor, stickyEl }, (progress) => {
4145
- writeProgress(ctx, progress);
4146
- const index = Math.min(frames - 1, Math.floor(progress * frames));
4147
- if (index === lastIndex) return;
4148
- lastIndex = index;
4149
- marker.mark(index);
4150
- });
4151
- marker.mark(0);
4152
- return () => {
4153
- untrack();
4154
- marker.restore();
4155
- };
4156
- }
4157
- function prepareSrcScrub(el, params, ctx, anchors) {
4158
- const { contentAnchor, stickyEl } = anchors;
4159
- const frames = Math.max(1, Math.round(params.num("frames", 1)));
4160
- const pattern = mediaSrcPattern(params.text("src"), ctx);
4161
- const media = el;
4162
- let lastIndex = -1;
4163
- const untrack = trackProgress(el, ctx, { distance: params.text("distance"), contentAnchor, stickyEl }, (progress) => {
4164
- writeProgress(ctx, progress);
4165
- const index = Math.min(frames - 1, Math.floor(progress * frames));
4166
- if (index === lastIndex) return;
4167
- lastIndex = index;
4168
- applyFrame(media, { index, frames, progress, pattern });
4169
- });
4170
- return untrack;
4171
- }
4172
- function mediaSrcPattern(pattern, ctx) {
4173
- if (!pattern || isSameOriginPath(pattern)) return pattern;
4174
- ctx.warn(`media-scrub "src" must be a same-origin path, got "${pattern}" \u2014 ignoring`);
4175
- return "";
4176
- }
4177
- function applyFrame(media, write) {
4178
- const { index, frames, progress, pattern } = write;
4179
- if (media.tagName === "VIDEO") {
4180
- const duration = Number.isFinite(media.duration) ? media.duration : 0;
4181
- if (duration > 0) media.currentTime = duration * progress;
4182
- return;
4183
- }
4184
- if (media.tagName !== "IMG") return;
4185
- if (pattern) media.src = pattern.replace("{i}", String(index).padStart(String(frames).length, "0"));
4186
- }
4187
- function prepareSmoothScroll(el, params, ctx) {
4188
- ctx.style.set("scroll-behavior", params.text("behavior", "smooth"));
4189
- return () => {
4190
- };
4191
- }
4192
- function prepareSnap(el, params, ctx) {
4193
- const axis = params.is("axis", "x") ? "x" : "y";
4194
- ctx.style.set("scroll-snap-type", `${axis} ${params.text("strictness", "mandatory")}`);
4195
- const selector = resolveTarget(params.text("target"), ctx, "scroll-snap");
4196
- const items = selector ? [...el.querySelectorAll(selector)] : [...el.children];
4197
- if (selector && items.length === 0) {
4198
- ctx.warn(`scroll-snap target "${selector}" matched nothing inside this element`);
4199
- }
4200
- if (selector) installSnapContainer(axis, ctx);
4201
- const childLedgers = items.map((child) => createStyleLedger(child));
4202
- for (const ledger of childLedgers) ledger.set("scroll-snap-align", params.text("align", "start"));
4203
- return () => {
4204
- for (const ledger of childLedgers) ledger.restore();
4205
- };
4206
- }
4207
- function installSnapContainer(axis, ctx) {
4208
- ctx.style.set(axis === "x" ? "overflow-x" : "overflow-y", "auto");
4209
- if (axis === "x") ctx.style.set("display", "flex");
4210
- }
4211
- var SCROLL_PRIMITIVES = [
4212
- scrollPrimitive({
4213
- id: "pin",
4214
- channels: ["layout", "progress"],
4215
- parameters: {
4216
- ...distanceParam,
4217
- ...stickyParams
4218
- },
4219
- prepare: deferPrepare(preparePin),
4220
- perfClass: "layout"
4221
- }),
4222
- scrollPrimitive({
4223
- id: "scroll-progress",
4224
- channels: ["progress"],
4225
- parameters: {
4226
- ...distanceParam,
4227
- steps: { type: "number", default: "0", cssProperty: "--kui-steps" },
4228
- // Same name, same shape and the same validation as scroll-spy's: one `target:` convention
4229
- // across the library rather than a second word for "the elements this effect marks".
4230
- target: { type: "text", default: "", cssProperty: "--kui-target" }
4231
- },
4232
- prepare: deferPrepare(prepareProgress)
4233
- }),
4234
- scrollPrimitive({
4235
- id: "horizontal-track",
4236
- channels: ["translate", "progress"],
4237
- parameters: {
4238
- ...distanceParam,
4239
- travel: { type: "text", default: "auto", cssProperty: "--kui-travel" },
4240
- // Same name, same shape and the same validation as scroll-spy's and media-scrub's: one
4241
- // `target:` convention across the library. Naming the row that moves is also what opts this
4242
- // primitive into owning the stage, the sticky window and the row's layout itself.
4243
- target: { type: "text", default: "", cssProperty: "--kui-target" },
4244
- "offset-top": {
4245
- type: "length",
4246
- default: "var(--kui-pin-offset, 0px)",
4247
- cssProperty: "--kui-offset-top"
4248
- }
4249
- },
4250
- prepare: deferPrepare(prepareHorizontal)
4251
- }),
4252
- scrollPrimitive({
4253
- id: "media-scrub",
4254
- channels: ["media", "progress"],
4255
- parameters: {
4256
- ...distanceParam,
4257
- // A scrub is a hold, so it needs the same two knobs a pin does. Declaring them here is what
4258
- // lets `sequence-scrub` become one attribute with no wrapper at all.
4259
- ...stickyParams,
4260
- frames: { type: "number", default: "1", cssProperty: "--kui-frames" },
4261
- src: { type: "text", default: "", cssProperty: "--kui-src" },
4262
- // The preferred form. `frames:`/`src:` remain for sequences too long to author as tags.
4263
- target: { type: "text", default: "", cssProperty: "--kui-target" }
4264
- },
4265
- prepare: deferPrepare(prepareMediaScrub),
4266
- perfClass: "paint"
4267
- }),
4268
- scrollPrimitive({
4269
- id: "scroll-spy",
4270
- channels: ["state"],
4271
- parameters: {
4272
- // `distance`: the per-section form only. `offset-top`: the container form only. Each is a
4273
- // no-op — warned, not silent — in the other; see `prepareScrollSpySingle` and
4274
- // `prepareScrollSpyContainer`.
4275
- ...distanceParam,
4276
- // Same name and meaning in both forms: the link(s) this instance marks. Per-section, the
4277
- // one link this section names; with `sections:`, every link `target:` matches, each paired
4278
- // to its own section by `href`. See `prepareScrollSpyContainer`.
4279
- target: { type: "text", default: "", cssProperty: "--kui-target" },
4280
- // Presence, not value, selects the container form: authoring this at all switches
4281
- // `prepareScrollSpy` from one-section-per-instance to one-instance-on-the-shared-ancestor.
4282
- sections: { type: "text", default: "", cssProperty: "--kui-sections" },
4283
- "offset-top": { type: "length", default: "0px", cssProperty: "--kui-offset-top" }
4284
- },
4285
- prepare: deferPrepare(prepareScrollSpy)
4286
- }),
4287
- scrollPrimitive({
4288
- id: "smooth-scroll",
4289
- /*
4290
- * Its own channel, not the `'layout'` it used to share with `pin`, `stacking-cards` and
4291
- * `scroll-snap`. The channel model exists to stop two effects fighting over the same CSS
4292
- * property, and this one writes exactly `scroll-behavior` — a property that describes how a
4293
- * *user-or-script-initiated* scroll is performed, and that no other primitive touches.
4294
- *
4295
- * On `'layout'` it made a legitimate pairing impossible. Both `smooth-scroll` and
4296
- * `scroll-snap` have to sit on the document element to have any effect at all — neither
4297
- * `scroll-behavior` nor `scroll-snap-type` is propagated to the viewport from `<body>` — so
4298
- * "apply them to nested elements", the advice the conflict message gives, has no valid
4299
- * nesting to offer here. `data-kui="smooth-scroll-to, scroll-snap-y"` on `<html>` is the
4300
- * ordinary way to ask for smooth anchor jumps on a page that also snaps, and it was refused
4301
- * for a collision that cannot happen: the two write disjoint properties.
4302
- */
4303
- channels: ["scroll-behavior"],
4304
- parameters: {
4305
- behavior: { type: "keyword", default: "smooth", cssProperty: "--kui-scroll-behavior", values: ["smooth", "auto"] }
4306
- },
4307
- prepare: deferPrepare(prepareSmoothScroll),
4308
- perfClass: "layout"
4309
- }),
4310
- scrollPrimitive({
4311
- id: "scroll-snap",
4312
- channels: ["layout"],
4313
- parameters: {
4314
- axis: { type: "keyword", default: "y", cssProperty: "--kui-axis", values: ["x", "y"] },
4315
- strictness: {
4316
- type: "keyword",
4317
- default: "mandatory",
4318
- cssProperty: "--kui-snap-strictness",
4319
- values: ["mandatory", "proximity"]
4320
- },
4321
- align: {
4322
- type: "keyword",
4323
- default: "start",
4324
- cssProperty: "--kui-snap-align",
4325
- values: ["start", "center", "end"]
4326
- },
4327
- // Names the snap items. Also what opts this primitive into owning the scroll container
4328
- // itself — see `installSnapContainer`. Without it, the direct children are the items and
4329
- // the page keeps its own `overflow`, exactly as before.
4330
- target: { type: "text", default: "", cssProperty: "--kui-target" }
4331
- },
4332
- prepare: deferPrepare(prepareSnap),
4333
- perfClass: "layout"
4334
- })
4335
- ];
4336
-
4337
- // src/effects/scroll-mechanics/presets.ts
4338
- var SCROLL_PRESETS = [
4339
- // --- pinning ---------------------------------------------------------------------------
4340
- // The default carries a spacer: a pin longer than its containing block silently does nothing,
4341
- // and that is the single most common way authors get sticky wrong.
4342
- { name: "pin-section", primitive: "pin", params: { distance: "100vh", spacer: "true" } },
4343
- { name: "pin-until", primitive: "pin", params: { spacer: "false" } },
4344
- { name: "pin-spacer", primitive: "pin", params: { spacer: "true" } },
4345
- // Applied per card; each card sticks at its own offset and the stack builds up naturally.
4346
- { name: "stacking-cards", primitive: "pin", params: { spacer: "false" } },
4347
- // --- progress publishing ---------------------------------------------------------------
4348
- { name: "scroll-progress", primitive: "scroll-progress" },
4349
- { name: "scrollytelling-step", primitive: "scroll-progress", params: { steps: "4" } },
4350
- // --- travel ------------------------------------------------------------------------------
4351
- { name: "horizontal-scroll", primitive: "horizontal-track" },
4352
- // --- media -------------------------------------------------------------------------------
4353
- /*
4354
- * `spacer:true` is what deletes the `.scrub-stage` wrapper a page used to hand-write.
4355
- *
4356
- * It could not be switched on until the tracker stopped measuring against the parent. A scrub
4357
- * makes itself sticky, and `geometrySource` escapes a sticky subtree by taking its parent — so
4358
- * with the wrapper gone the scrub was measured against whatever section contained it. Measured
4359
- * on `demo/scroll.html`: the parent started 926px above the scrub against a 1817px distance, so
4360
- * progress reached 51% before the element had even stuck and half the sequence played off
4361
- * screen. The wrapper was not ceremony; it was the tight box that made the parent honest.
4362
- *
4363
- * `trackProgress`'s `contentAnchor` is the fix. Progress is read from the spacer, which the
4364
- * library inserts, is exactly `distance` tall, is never sticky, and moves with the content — so
4365
- * there is no wrapper to write and nothing to disagree with.
4366
- *
4367
- * `video-scrub` stays off: a video positioned by the page is not asking the library for a box.
4368
- */
4369
- { name: "sequence-scrub", primitive: "media-scrub", params: { spacer: "true" } },
4370
- { name: "video-scrub", primitive: "media-scrub" },
4371
- // --- navigation --------------------------------------------------------------------------
4372
- { name: "scroll-spy", primitive: "scroll-spy" },
4373
- // --- native CSS passthroughs ---------------------------------------------------------------
4374
- { name: "smooth-scroll-to", primitive: "smooth-scroll" },
4375
- { name: "scroll-snap-x", primitive: "scroll-snap", params: { axis: "x" } },
4376
- { name: "scroll-snap-y", primitive: "scroll-snap", params: { axis: "y" } }
4377
- ];
4378
-
4379
- // src/effects/scroll-mechanics/index.ts
4380
- function registerScrollMechanics(registry) {
4381
- return registry.registerPrimitives(SCROLL_PRIMITIVES).registerPresets(SCROLL_PRESETS);
4382
- }
4383
-
4384
- // src/core/path-morph.ts
4385
- var COMMAND = /([mlhvcz])|(-?(?:\d+(?:\.\d+)?|\.\d+))/gi;
4386
- var UNSUPPORTED = /[AaSsQqTt]/;
4387
- function parsePath(d) {
4388
- if (UNSUPPORTED.test(d)) {
4389
- return {
4390
- segments: [],
4391
- subpaths: [],
4392
- reason: "arc and shorthand commands (A S Q T) are not supported"
4393
- };
4394
- }
4395
- const tokens = [...d.matchAll(COMMAND)].map((m) => m[1] ?? m[2]);
4396
- const state = {
4397
- subpaths: [],
4398
- open: void 0,
4399
- current: { x: 0, y: 0 },
4400
- start: { x: 0, y: 0 },
4401
- command: ""
4402
- };
4403
- let index = 0;
4404
- while (index < tokens.length) {
4405
- const token = tokens[index];
4406
- if (/[a-z]/i.test(token)) {
4407
- state.command = token;
4408
- if (token.toLowerCase() === "z") closeSubpath(state);
4409
- index++;
4410
- continue;
4411
- }
4412
- if (!state.command) {
4413
- return { segments: [], subpaths: [], reason: "path must start with a command letter" };
4414
- }
4415
- index = consume(tokens, index, state);
4416
- }
4417
- if (state.subpaths.length === 0) {
4418
- return { segments: [], subpaths: [], reason: "no drawable segments" };
4419
- }
4420
- return { segments: state.subpaths.flatMap((sub) => sub.segments), subpaths: state.subpaths };
4421
- }
4422
- function pushSegment(state, segment) {
4423
- if (!state.open) {
4424
- state.open = { segments: [], closed: false };
4425
- state.subpaths.push(state.open);
4426
- }
4427
- state.open.segments.push(segment);
4428
- }
4429
- var ARITY = { m: 2, l: 2, h: 1, v: 1, c: 6, z: 0 };
4430
- function closeSubpath(state) {
4431
- const { current, start, open } = state;
4432
- if (!open) return;
4433
- if (Math.abs(current.x - start.x) >= 1e-6 || Math.abs(current.y - start.y) >= 1e-6) {
4434
- open.segments.push(lineToCubic(current, start));
4435
- }
4436
- open.closed = true;
4437
- state.current = { ...start };
4438
- state.open = void 0;
4439
- }
4440
- function consume(tokens, index, state) {
4441
- const key = state.command.toLowerCase();
4442
- const relative = state.command === key;
4443
- const arity = ARITY[key];
4444
- const args = tokens.slice(index, index + arity).map(Number);
4445
- if (args.length < arity) return tokens.length;
4446
- const next = endpointFor(key, args, state.current, relative);
4447
- if (key === "m") {
4448
- state.current = next;
4449
- state.start = next;
4450
- state.open = void 0;
4451
- state.command = relative ? "l" : "L";
4452
- return index + arity;
4453
- }
4454
- pushSegment(state, straightOrCubic({ key, args, from: state.current, relative }, next));
4455
- state.current = next;
4456
- return index + arity;
4457
- }
4458
- function endpointFor(key, args, current, relative) {
4459
- const base = relative ? current : { x: 0, y: 0 };
4460
- if (key === "h") return { x: base.x + args[0], y: current.y };
4461
- if (key === "v") return { x: current.x, y: base.y + args[0] };
4462
- if (key === "c") return { x: base.x + args[4], y: base.y + args[5] };
4463
- return { x: base.x + args[0], y: base.y + args[1] };
4464
- }
4465
- function straightOrCubic(command, to) {
4466
- const { key, args, from, relative } = command;
4467
- if (key !== "c") return lineToCubic(from, to);
4468
- const base = relative ? from : { x: 0, y: 0 };
4469
- return {
4470
- from,
4471
- c1: { x: base.x + args[0], y: base.y + args[1] },
4472
- c2: { x: base.x + args[2], y: base.y + args[3] },
4473
- to
4474
- };
4475
- }
4476
- function lineToCubic(from, to) {
4477
- return {
4478
- from,
4479
- c1: lerpPoint(from, to, 1 / 3),
4480
- c2: lerpPoint(from, to, 2 / 3),
4481
- to
4482
- };
4483
- }
4484
- function lerp(a, b, t) {
4485
- return a + (b - a) * t;
4486
- }
4487
- function lerpPoint(a, b, t) {
4488
- return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) };
4489
- }
4490
- function splitCubic(segment, t) {
4491
- const p01 = lerpPoint(segment.from, segment.c1, t);
4492
- const p12 = lerpPoint(segment.c1, segment.c2, t);
4493
- const p23 = lerpPoint(segment.c2, segment.to, t);
4494
- const p012 = lerpPoint(p01, p12, t);
4495
- const p123 = lerpPoint(p12, p23, t);
4496
- const mid = lerpPoint(p012, p123, t);
4497
- return [
4498
- { from: segment.from, c1: p01, c2: p012, to: mid },
4499
- { from: mid, c1: p123, c2: p23, to: segment.to }
4500
- ];
4501
- }
4502
- function normaliseCount(segments, target) {
4503
- const out = [...segments];
4504
- while (out.length < target && out.length > 0) {
4505
- let longest = 0;
4506
- for (let i = 1; i < out.length; i++) {
4507
- if (chordLength(out[i]) > chordLength(out[longest])) longest = i;
4508
- }
4509
- const [a, b] = splitCubic(out[longest], 0.5);
4510
- out.splice(longest, 1, a, b);
4511
- }
4512
- return out;
4513
- }
4514
- function chordLength(segment) {
4515
- return Math.hypot(segment.to.x - segment.from.x, segment.to.y - segment.from.y);
4516
- }
4517
- function round(value) {
4518
- return Math.round(value * 100) / 100;
4519
- }
4520
- function subpathsToPathData(subpaths) {
4521
- const parts = [];
4522
- for (const sub of subpaths) {
4523
- const head = sub.segments[0];
4524
- parts.push(`M${round(head.from.x)},${round(head.from.y)}`);
4525
- for (const s of sub.segments) {
4526
- parts.push(
4527
- `C${round(s.c1.x)},${round(s.c1.y)} ${round(s.c2.x)},${round(s.c2.y)} ${round(s.to.x)},${round(s.to.y)}`
4528
- );
4529
- }
4530
- if (sub.closed) parts.push("Z");
4531
- }
4532
- return parts.join(" ");
4533
- }
4534
- function centroidOf(subpath) {
4535
- let x = 0;
4536
- let y = 0;
4537
- for (const s of subpath.segments) {
4538
- x += s.from.x;
4539
- y += s.from.y;
4540
- }
4541
- return { x: x / subpath.segments.length, y: y / subpath.segments.length };
4542
- }
4543
- function collapsedLike(partner) {
4544
- const at = centroidOf(partner);
4545
- return {
4546
- segments: partner.segments.map(() => ({
4547
- from: { ...at },
4548
- c1: { ...at },
4549
- c2: { ...at },
4550
- to: { ...at }
4551
- })),
4552
- closed: partner.closed
4553
- };
4554
- }
4555
- function normaliseSubpaths(a, b) {
4556
- const count = Math.max(a.length, b.length);
4557
- const from = [];
4558
- const to = [];
4559
- for (let i = 0; i < count; i++) {
4560
- const left = a[i] ?? collapsedLike(b[i]);
4561
- const right = b[i] ?? collapsedLike(a[i]);
4562
- const target = Math.max(left.segments.length, right.segments.length);
4563
- const closed = left.closed && right.closed;
4564
- from.push({ segments: normaliseCount(left.segments, target), closed });
4565
- to.push({ segments: normaliseCount(right.segments, target), closed });
4566
- }
4567
- return { from, to };
4568
- }
4569
- function createMorph(fromPath, toPath) {
4570
- const a = parsePath(fromPath);
4571
- const b = parsePath(toPath);
4572
- if (a.reason) return { reason: `start path: ${a.reason}` };
4573
- if (b.reason) return { reason: `end path: ${b.reason}` };
4574
- const { from, to } = normaliseSubpaths(a.subpaths, b.subpaths);
4575
- const count = from.reduce((total, sub) => total + sub.segments.length, 0);
4576
- return {
4577
- morph: {
4578
- segmentCount: count,
4579
- at(t) {
4580
- const clamped = Math.min(1, Math.max(0, t));
4581
- return subpathsToPathData(
4582
- from.map((sub, i) => ({
4583
- segments: sub.segments.map(
4584
- (segment, j) => lerpCubic(segment, to[i].segments[j], clamped)
4585
- ),
4586
- closed: sub.closed
4587
- }))
4588
- );
4589
- }
4590
- }
4591
- };
4592
- }
4593
- function lerpCubic(a, b, t) {
4594
- return {
4595
- from: lerpPoint(a.from, b.from, t),
4596
- c1: lerpPoint(a.c1, b.c1, t),
4597
- c2: lerpPoint(a.c2, b.c2, t),
4598
- to: lerpPoint(a.to, b.to, t)
4599
- };
4600
- }
4601
-
4602
- // src/effects/svg/index.ts
4603
- function prepareMorph(el, params, ctx) {
4604
- const path = el;
4605
- const startPath = params.text("from") || path.getAttribute("d") || "";
4606
- const { morph, reason } = createMorph(startPath, params.text("to"));
4607
- if (!morph) {
4608
- ctx.warn(`cannot morph: ${reason}`);
4609
- return () => {
4610
- };
4611
- }
4612
- const duration = effectDurationMs(params, 300);
4613
- let frame = 0;
4614
- let cancelled = false;
4615
- const drive = (target) => {
4616
- cancelAnimationFrame(frame);
4617
- const startedAt = performance.now();
4618
- const startValue = current;
4619
- const step = (now) => {
4620
- if (cancelled) return;
4621
- const t = duration > 0 ? Math.min(1, (now - startedAt) / duration) : 1;
4622
- current = startValue + (target - startValue) * t;
4623
- path.setAttribute("d", morph.at(current));
4624
- if (t < 1) frame = requestAnimationFrame(step);
4625
- };
4626
- frame = requestAnimationFrame(step);
4627
- };
4628
- let current = 0;
4629
- const enter = () => drive(1);
4630
- const leave = () => drive(0);
4631
- el.addEventListener("pointerenter", enter, { passive: true });
4632
- el.addEventListener("focusin", enter, { passive: true });
4633
- el.addEventListener("pointerleave", leave, { passive: true });
4634
- el.addEventListener("focusout", leave, { passive: true });
4635
- return () => {
4636
- cancelled = true;
4637
- cancelAnimationFrame(frame);
4638
- el.removeEventListener("pointerenter", enter);
4639
- el.removeEventListener("focusin", enter);
4640
- el.removeEventListener("pointerleave", leave);
4641
- el.removeEventListener("focusout", leave);
4642
- path.setAttribute("d", startPath);
4643
- };
4644
- }
4645
- var PATH_DRAW_PRIMITIVE = cssPrimitive("path-draw", [CHANNEL.stroke], {
4646
- parameters: {
4647
- length: { type: "number", default: "100", cssProperty: "--kui-path-length", finite: true }
4648
- }
4649
- });
4650
- var SHAPE_FILL_PRIMITIVE = cssPrimitive("shape-fill", [CHANNEL.clip]);
4651
- var BAR_GROW_PRIMITIVE = cssPrimitive("bar-grow", [CHANNEL.scale], {
4652
- parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
4653
- });
4654
- var LOGO_BUILD_PRIMITIVE = cssPrimitive("logo-assemble", [
4655
- CHANNEL.opacity,
4656
- CHANNEL.scale,
4657
- CHANNEL.rotate
4658
- ]);
4659
- var ICON_TOGGLE_PRIMITIVE = {
4660
- id: "icon-toggle",
4661
- renderer: "javascript",
4662
- channels: [CHANNEL.translate, CHANNEL.rotate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.clip],
4663
- parameters: {
4664
- duration: { type: "time", default: "260ms", cssProperty: "--kui-duration" },
4665
- ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
4666
- },
4667
- supportedTimelines: ["time"],
4668
- supportedActivations: ["load"],
4669
- defaultActivation: "load",
4670
- perfClass: "compositor",
4671
- reducedMotion: "disable",
4672
- prepare: () => inertInstance()
4673
- };
4674
- var SVG_PRIMITIVES = [
4675
- {
4676
- id: "path-morph",
4677
- renderer: "javascript",
4678
- // `d` is its own channel: nothing else in the catalog writes path geometry, so a morph can
4679
- // safely compose with a fade or a rotation on the same element.
4680
- channels: ["path"],
4681
- parameters: {
4682
- from: { type: "text", default: "", cssProperty: "--kui-path-from" },
4683
- to: { type: "text", default: "", cssProperty: "--kui-path-to" },
4684
- duration: { type: "time", default: "300ms", cssProperty: "--kui-duration" }
4685
- },
4686
- supportedTimelines: ["time"],
4687
- supportedActivations: ["hover", "focus", "manual", "load"],
4688
- // The morph attaches its own hover listeners, so it must be wired up on load, not on enter.
4689
- defaultActivation: "load",
4690
- perfClass: "paint",
4691
- reducedMotion: "disable",
4692
- prepare: deferPrepare(prepareMorph)
4693
- },
4694
- PATH_DRAW_PRIMITIVE,
4695
- SHAPE_FILL_PRIMITIVE,
4696
- BAR_GROW_PRIMITIVE,
4697
- LOGO_BUILD_PRIMITIVE,
4698
- ICON_TOGGLE_PRIMITIVE
4699
- ];
4700
- var SVG_PRESETS = [
4701
- { name: "icon-morph", primitive: "path-morph" },
4702
- { name: "blob-morph", primitive: "path-morph", params: { duration: "800ms" } },
4703
- // Stroke draws. One keyframe block each rather than one shared block, matching the
4704
- // progress-ring/gauge-sweep/donut-sweep/sparkline-draw group in numbers.ts: identical bodies
4705
- // today, but each name is free to diverge and a consumer can restyle one without the others.
4706
- { name: "draw-stroke", primitive: "path-draw", keyframes: "kui-draw-stroke", params: { duration: "800ms", ease: "ease-in-out" } },
4707
- { name: "draw-signature", primitive: "path-draw", keyframes: "kui-draw-signature", params: { duration: "1600ms", ease: "ease-in-out" } },
4708
- { name: "draw-underline", primitive: "path-draw", keyframes: "kui-draw-underline", params: { duration: "420ms" } },
4709
- { name: "checkmark-draw", primitive: "path-draw", keyframes: "kui-checkmark-draw", params: { duration: "320ms" } },
4710
- { name: "cross-draw", primitive: "path-draw", keyframes: "kui-cross-draw", params: { duration: "260ms" } },
4711
- { name: "chart-line-draw", primitive: "path-draw", keyframes: "kui-chart-line-draw", params: { duration: "1200ms", ease: "ease-in-out" } },
4712
- { name: "gradient-stroke", primitive: "path-draw", keyframes: "kui-gradient-stroke", params: { duration: "2400ms", ease: "ease-in-out" } },
4713
- // Fills.
4714
- { name: "heart-fill", primitive: "shape-fill", keyframes: "kui-heart-fill", params: { duration: "420ms" } },
4715
- { name: "bookmark-fill", primitive: "shape-fill", keyframes: "kui-bookmark-fill", params: { duration: "360ms" } },
4716
- { name: "chart-area-fill", primitive: "shape-fill", keyframes: "kui-chart-area-fill", params: { duration: "900ms" } },
4717
- // `cloak: true`: `kui-chart-bar-grow`'s `from { scale: 1 0 }` (svg.css) is a zero-height box
4718
- // while paused, not just an invisible one — so it occupies no space in layout for the whole
4719
- // wait. Same defect `fold-panel` has, and the same fix shape: see svg.css's
4720
- // `[data-kui-fx~='chart-bar-grow'][data-kui-state='ready']` rule.
4721
- {
4722
- name: "chart-bar-grow",
4723
- primitive: "bar-grow",
4724
- keyframes: "kui-chart-bar-grow",
4725
- params: { duration: "700ms", ease: "back-out" },
4726
- cloak: true
4727
- },
4728
- { name: "logo-build", primitive: "logo-assemble", keyframes: "kui-logo-build", params: { duration: "520ms", ease: "back-out" } },
4729
- // Icon toggles — no `keyframes`, because their motion is a CSS transition in svg.css keyed off
4730
- // aria state, not a compiled animation. Same shape as forms.ts's native-state presets.
4731
- { name: "hamburger-to-x", primitive: "icon-toggle" },
4732
- { name: "play-to-pause", primitive: "icon-toggle" },
4733
- { name: "plus-to-minus", primitive: "icon-toggle" }
4734
- ];
4735
- function registerSvg(registry) {
4736
- return registry.registerPrimitives(SVG_PRIMITIVES).registerPresets(SVG_PRESETS);
4737
- }
4738
-
4739
- // src/effects/three-d/index.ts
4740
- var FLIP_CONTROL_SELECTOR = ":scope > .kui-flip-control";
4741
- function prepareCardToggle(el, params, ctx) {
4742
- const trigger = params.text("trigger", "click");
4743
- if (trigger === "click") return () => {
4744
- };
4745
- if (!supportsFineHover(ctx.win)) return () => {
4746
- };
4747
- const control = el.querySelector(FLIP_CONTROL_SELECTOR);
4748
- if (!control) {
4749
- ctx.warn(`flip-card trigger:${trigger} found no direct-child .kui-flip-control \u2014 the card will not flip`);
4750
- return () => {
4751
- };
4752
- }
4753
- const set = (flipped) => control.setAttribute("aria-pressed", String(flipped));
4754
- const isFlipped = () => control.getAttribute("aria-pressed") === "true";
4755
- const onEnter = () => {
4756
- set(trigger === "hover-toggle" ? !isFlipped() : true);
4757
- };
4758
- const onLeave = () => set(false);
4759
- el.addEventListener("pointerenter", onEnter, { passive: true });
4760
- if (trigger === "hover") el.addEventListener("pointerleave", onLeave, { passive: true });
4761
- return () => {
4762
- el.removeEventListener("pointerenter", onEnter);
4763
- el.removeEventListener("pointerleave", onLeave);
4764
- };
4765
- }
4766
- var CARD_TOGGLE_PRIMITIVE = {
4767
- id: "card-toggle",
4768
- renderer: "javascript",
4769
- channels: [CHANNEL.rotate],
4770
- parameters: {
4771
- duration: { type: "time", default: "700ms", cssProperty: "--kui-duration" },
4772
- ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
4773
- perspective: { type: "length", default: "1600px", cssProperty: "--kui-perspective" },
4774
- trigger: {
4775
- type: "keyword",
4776
- default: "click",
4777
- cssProperty: "--kui-flip-trigger",
4778
- values: ["click", "hover", "hover-latch", "hover-toggle"]
4779
- }
4780
- },
4781
- supportedTimelines: ["time"],
4782
- supportedActivations: ["load"],
4783
- defaultActivation: "load",
4784
- perfClass: "compositor",
4785
- reducedMotion: "disable",
4786
- prepare: deferPrepare(prepareCardToggle)
4787
- };
4788
- var THREE_D_PRIMITIVES = [
4789
- // `skew`, not `rotate`: the keyframes in three-d.css write `transform: perspective(...)
4790
- // rotateX/Y(...)`, not the individual `rotate:` property — `perspective` only creates depth for
4791
- // an element's *children*, so giving one of these effects its own depth means reaching for the
4792
- // `perspective()` transform *function*, which only exists inside the `transform` shorthand.
4793
- // `CHANNEL.skew` is this catalog's name for "claims the whole `transform` shorthand"; see the
4794
- // comment on it in `core/types.ts`.
4795
- cssPrimitive("flip-face", [CHANNEL.skew], {
4796
- parameters: {
4797
- angle: { type: "angle", default: "180deg", cssProperty: "--kui-from-angle" },
4798
- perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
4799
- }
4800
- }),
4801
- cssPrimitive("page-reveal", [CHANNEL.opacity, CHANNEL.translate], {
4802
- parameters: { distance: { type: "length", default: "40px", cssProperty: "--kui-distance" } }
4803
- }),
4804
- cssPrimitive("wipe", [CHANNEL.clip]),
4805
- // `from` gives `loading-bar` a real knob on its start scale — it had none before, unlike
4806
- // `flip-face`'s `angle:` two rows up. `--kui-bar-from` is also what the fold-panel-style
4807
- // `[data-kui-fx~='loading-bar'][data-kui-state='ready']` gate neutralizes in three-d.css; see
4808
- // that rule's comment for the on:enter fix this parameter doubles as.
4809
- cssPrimitive("bar", [CHANNEL.scale], {
4810
- parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
4811
- }),
4812
- CARD_TOGGLE_PRIMITIVE
4813
- ];
4814
- var THREE_D_PRESETS = [
4815
- // --- 3D & perspective ---
4816
- { name: "card-flip-y", primitive: "flip-face", keyframes: "kui-card-flip-y", cloak: true },
4817
- { name: "card-flip-x", primitive: "flip-face", keyframes: "kui-card-flip-x", cloak: true },
4818
- { name: "cube-rotate", primitive: "flip-face", keyframes: "kui-cube-rotate", params: { angle: "90deg" } },
4819
- {
4820
- name: "book-page-turn",
4821
- primitive: "flip-face",
4822
- keyframes: "kui-book-page-turn",
4823
- params: { angle: "-160deg", duration: "900ms" }
4824
- },
4825
- // `cloak: true`, unlike its `flip-face` siblings above: those are `to`-only keyframes, so their
4826
- // paused/waiting box is the ordinary, untransformed rest state. `fold-panel` is `from`-only —
4827
- // its `rotateX(-90deg)` (three-d.css) *is* the paused box, edge-on and zero-height, so it holds
4828
- // no space in layout for the whole wait; see the
4829
- // `[data-kui-fx~='fold-panel'][data-kui-state='ready']` rule in three-d.css for the other half
4830
- // of that fix. `cloak` only ever hid the pre-JS flash, not this, but adding it here keeps the
4831
- // pre-JS and post-JS "ready" appearances the same (invisible) instead of trading one flash for
4832
- // the other.
4833
- {
4834
- name: "fold-panel",
4835
- primitive: "flip-face",
4836
- keyframes: "kui-fold-panel",
4837
- params: { angle: "-90deg" },
4838
- cloak: true
4839
- },
4840
- // --- page transitions ---
4841
- { name: "page-fade", primitive: "page-reveal", keyframes: "kui-page-fade" },
4842
- { name: "page-slide", primitive: "page-reveal", keyframes: "kui-page-slide" },
4843
- { name: "curtain-wipe", primitive: "wipe", keyframes: "kui-curtain-wipe", params: { duration: "800ms" } },
4844
- // `cloak: true` for the same reason as `fold-panel`: `kui-loading-bar`'s `from { scale: 0 1 }`
4845
- // (three-d.css) is a zero-width box, not just an invisible one — see that file's
4846
- // `[data-kui-fx~='loading-bar'][data-kui-state='ready']` rule.
4847
- { name: "loading-bar", primitive: "bar", keyframes: "kui-loading-bar", cloak: true },
4848
- // No `keyframes`: its motion is a CSS transition in three-d.css keyed off the control's
4849
- // aria-pressed, not a compiled animation. Same shape as the icon toggles in svg.ts.
4850
- { name: "flip-card", primitive: "card-toggle" }
4851
- ];
4852
- function registerThreeD(registry) {
4853
- return registry.registerPrimitives(THREE_D_PRIMITIVES).registerPresets(THREE_D_PRESETS);
4854
- }
4855
-
4856
- // src/effects/index.ts
4857
- function createRegistry() {
4858
- const registry = new Registry();
4859
- registerCore(registry);
4860
- registerScrollMechanics(registry);
4861
- registerLayout(registry);
4862
- registerSvg(registry);
4863
- registerGestures(registry);
4864
- registerThreeD(registry);
4865
- registerCatalog(registry);
4866
- registerNavigation(registry);
4867
- registerForms(registry);
4868
- return registry;
4869
- }
4870
-
4871
- export {
4872
- registerCatalog,
4873
- PRIMITIVES,
4874
- PRESETS,
4875
- COMBOS,
4876
- registerCore,
4877
- registerGestures,
4878
- registerLayout,
4879
- registerScrollMechanics,
4880
- registerSvg,
4881
- registerThreeD,
4882
- createRegistry
4883
- };