kuinetic 0.1.3 → 0.2.0

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