@bquery/bquery 1.1.2 → 1.3.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 (307) hide show
  1. package/README.md +501 -323
  2. package/dist/batch-4LAvfLE7.js +13 -0
  3. package/dist/batch-4LAvfLE7.js.map +1 -0
  4. package/dist/component/component.d.ts +69 -0
  5. package/dist/component/component.d.ts.map +1 -0
  6. package/dist/component/html.d.ts +35 -0
  7. package/dist/component/html.d.ts.map +1 -0
  8. package/dist/component/index.d.ts +3 -126
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/props.d.ts +18 -0
  11. package/dist/component/props.d.ts.map +1 -0
  12. package/dist/component/types.d.ts +77 -0
  13. package/dist/component/types.d.ts.map +1 -0
  14. package/dist/component.es.mjs +90 -59
  15. package/dist/component.es.mjs.map +1 -1
  16. package/dist/core/collection.d.ts +36 -0
  17. package/dist/core/collection.d.ts.map +1 -1
  18. package/dist/core/dom.d.ts +6 -0
  19. package/dist/core/dom.d.ts.map +1 -0
  20. package/dist/core/element.d.ts +8 -0
  21. package/dist/core/element.d.ts.map +1 -1
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/utils/array.d.ts +74 -0
  25. package/dist/core/utils/array.d.ts.map +1 -0
  26. package/dist/core/utils/function.d.ts +70 -0
  27. package/dist/core/utils/function.d.ts.map +1 -0
  28. package/dist/core/utils/index.d.ts +70 -0
  29. package/dist/core/utils/index.d.ts.map +1 -0
  30. package/dist/core/utils/misc.d.ts +63 -0
  31. package/dist/core/utils/misc.d.ts.map +1 -0
  32. package/dist/core/utils/number.d.ts +65 -0
  33. package/dist/core/utils/number.d.ts.map +1 -0
  34. package/dist/core/utils/object.d.ts +133 -0
  35. package/dist/core/utils/object.d.ts.map +1 -0
  36. package/dist/core/utils/string.d.ts +80 -0
  37. package/dist/core/utils/string.d.ts.map +1 -0
  38. package/dist/core/utils/type-guards.d.ts +79 -0
  39. package/dist/core/utils/type-guards.d.ts.map +1 -0
  40. package/dist/core-COenAZjD.js +145 -0
  41. package/dist/core-COenAZjD.js.map +1 -0
  42. package/dist/core.es.mjs +411 -448
  43. package/dist/core.es.mjs.map +1 -1
  44. package/dist/full.d.ts +8 -2
  45. package/dist/full.d.ts.map +1 -1
  46. package/dist/full.es.mjs +86 -40
  47. package/dist/full.es.mjs.map +1 -1
  48. package/dist/full.iife.js +6 -1
  49. package/dist/full.iife.js.map +1 -1
  50. package/dist/full.umd.js +6 -1
  51. package/dist/full.umd.js.map +1 -1
  52. package/dist/index.d.ts +3 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.es.mjs +137 -44
  55. package/dist/index.es.mjs.map +1 -1
  56. package/dist/motion/animate.d.ts +25 -0
  57. package/dist/motion/animate.d.ts.map +1 -0
  58. package/dist/motion/easing.d.ts +30 -0
  59. package/dist/motion/easing.d.ts.map +1 -0
  60. package/dist/motion/flip.d.ts +55 -0
  61. package/dist/motion/flip.d.ts.map +1 -0
  62. package/dist/motion/index.d.ts +11 -138
  63. package/dist/motion/index.d.ts.map +1 -1
  64. package/dist/motion/keyframes.d.ts +21 -0
  65. package/dist/motion/keyframes.d.ts.map +1 -0
  66. package/dist/motion/reduced-motion.d.ts +12 -0
  67. package/dist/motion/reduced-motion.d.ts.map +1 -0
  68. package/dist/motion/scroll.d.ts +15 -0
  69. package/dist/motion/scroll.d.ts.map +1 -0
  70. package/dist/motion/spring.d.ts +42 -0
  71. package/dist/motion/spring.d.ts.map +1 -0
  72. package/dist/motion/stagger.d.ts +22 -0
  73. package/dist/motion/stagger.d.ts.map +1 -0
  74. package/dist/motion/timeline.d.ts +21 -0
  75. package/dist/motion/timeline.d.ts.map +1 -0
  76. package/dist/motion/transition.d.ts +22 -0
  77. package/dist/motion/transition.d.ts.map +1 -0
  78. package/dist/motion/types.d.ts +182 -0
  79. package/dist/motion/types.d.ts.map +1 -0
  80. package/dist/motion.es.mjs +320 -61
  81. package/dist/motion.es.mjs.map +1 -1
  82. package/dist/persisted-Dz_ryNuC.js +278 -0
  83. package/dist/persisted-Dz_ryNuC.js.map +1 -0
  84. package/dist/reactive/batch.d.ts +13 -0
  85. package/dist/reactive/batch.d.ts.map +1 -0
  86. package/dist/reactive/computed.d.ts +50 -0
  87. package/dist/reactive/computed.d.ts.map +1 -0
  88. package/dist/reactive/core.d.ts +60 -0
  89. package/dist/reactive/core.d.ts.map +1 -0
  90. package/dist/reactive/effect.d.ts +15 -0
  91. package/dist/reactive/effect.d.ts.map +1 -0
  92. package/dist/reactive/index.d.ts +2 -2
  93. package/dist/reactive/index.d.ts.map +1 -1
  94. package/dist/reactive/internals.d.ts +36 -0
  95. package/dist/reactive/internals.d.ts.map +1 -0
  96. package/dist/reactive/linked.d.ts +36 -0
  97. package/dist/reactive/linked.d.ts.map +1 -0
  98. package/dist/reactive/persisted.d.ts +14 -0
  99. package/dist/reactive/persisted.d.ts.map +1 -0
  100. package/dist/reactive/readonly.d.ts +26 -0
  101. package/dist/reactive/readonly.d.ts.map +1 -0
  102. package/dist/reactive/signal.d.ts +13 -305
  103. package/dist/reactive/signal.d.ts.map +1 -1
  104. package/dist/reactive/type-guards.d.ts +20 -0
  105. package/dist/reactive/type-guards.d.ts.map +1 -0
  106. package/dist/reactive/untrack.d.ts +29 -0
  107. package/dist/reactive/untrack.d.ts.map +1 -0
  108. package/dist/reactive/watch.d.ts +42 -0
  109. package/dist/reactive/watch.d.ts.map +1 -0
  110. package/dist/reactive.es.mjs +30 -154
  111. package/dist/reactive.es.mjs.map +1 -1
  112. package/dist/router/index.d.ts +41 -0
  113. package/dist/router/index.d.ts.map +1 -0
  114. package/dist/router/links.d.ts +44 -0
  115. package/dist/router/links.d.ts.map +1 -0
  116. package/dist/router/match.d.ts +20 -0
  117. package/dist/router/match.d.ts.map +1 -0
  118. package/dist/router/navigation.d.ts +45 -0
  119. package/dist/router/navigation.d.ts.map +1 -0
  120. package/dist/router/query.d.ts +16 -0
  121. package/dist/router/query.d.ts.map +1 -0
  122. package/dist/router/router.d.ts +34 -0
  123. package/dist/router/router.d.ts.map +1 -0
  124. package/dist/router/state.d.ts +27 -0
  125. package/dist/router/state.d.ts.map +1 -0
  126. package/dist/router/types.d.ts +88 -0
  127. package/dist/router/types.d.ts.map +1 -0
  128. package/dist/router/utils.d.ts +65 -0
  129. package/dist/router/utils.d.ts.map +1 -0
  130. package/dist/router.es.mjs +202 -0
  131. package/dist/router.es.mjs.map +1 -0
  132. package/dist/sanitize-1FBEPAFH.js +272 -0
  133. package/dist/sanitize-1FBEPAFH.js.map +1 -0
  134. package/dist/security/constants.d.ts +42 -0
  135. package/dist/security/constants.d.ts.map +1 -0
  136. package/dist/security/csp.d.ts +24 -0
  137. package/dist/security/csp.d.ts.map +1 -0
  138. package/dist/security/index.d.ts +4 -2
  139. package/dist/security/index.d.ts.map +1 -1
  140. package/dist/security/sanitize-core.d.ts +13 -0
  141. package/dist/security/sanitize-core.d.ts.map +1 -0
  142. package/dist/security/sanitize.d.ts +5 -57
  143. package/dist/security/sanitize.d.ts.map +1 -1
  144. package/dist/security/trusted-types.d.ts +25 -0
  145. package/dist/security/trusted-types.d.ts.map +1 -0
  146. package/dist/security/types.d.ts +36 -0
  147. package/dist/security/types.d.ts.map +1 -0
  148. package/dist/security.es.mjs +50 -277
  149. package/dist/security.es.mjs.map +1 -1
  150. package/dist/store/create-store.d.ts +15 -0
  151. package/dist/store/create-store.d.ts.map +1 -0
  152. package/dist/store/define-store.d.ts +28 -0
  153. package/dist/store/define-store.d.ts.map +1 -0
  154. package/dist/store/devtools.d.ts +22 -0
  155. package/dist/store/devtools.d.ts.map +1 -0
  156. package/dist/store/index.d.ts +12 -0
  157. package/dist/store/index.d.ts.map +1 -0
  158. package/dist/store/mapping.d.ts +28 -0
  159. package/dist/store/mapping.d.ts.map +1 -0
  160. package/dist/store/persisted.d.ts +13 -0
  161. package/dist/store/persisted.d.ts.map +1 -0
  162. package/dist/store/plugins.d.ts +13 -0
  163. package/dist/store/plugins.d.ts.map +1 -0
  164. package/dist/store/registry.d.ts +28 -0
  165. package/dist/store/registry.d.ts.map +1 -0
  166. package/dist/store/types.d.ts +71 -0
  167. package/dist/store/types.d.ts.map +1 -0
  168. package/dist/store/utils.d.ts +28 -0
  169. package/dist/store/utils.d.ts.map +1 -0
  170. package/dist/store/watch.d.ts +23 -0
  171. package/dist/store/watch.d.ts.map +1 -0
  172. package/dist/store.es.mjs +27 -0
  173. package/dist/store.es.mjs.map +1 -0
  174. package/dist/type-guards-DRma3-Kc.js +16 -0
  175. package/dist/type-guards-DRma3-Kc.js.map +1 -0
  176. package/dist/untrack-BuEQKH7_.js +6 -0
  177. package/dist/untrack-BuEQKH7_.js.map +1 -0
  178. package/dist/view/directives/bind.d.ts +7 -0
  179. package/dist/view/directives/bind.d.ts.map +1 -0
  180. package/dist/view/directives/class.d.ts +8 -0
  181. package/dist/view/directives/class.d.ts.map +1 -0
  182. package/dist/view/directives/for.d.ts +23 -0
  183. package/dist/view/directives/for.d.ts.map +1 -0
  184. package/dist/view/directives/html.d.ts +7 -0
  185. package/dist/view/directives/html.d.ts.map +1 -0
  186. package/dist/view/directives/if.d.ts +7 -0
  187. package/dist/view/directives/if.d.ts.map +1 -0
  188. package/dist/view/directives/index.d.ts +12 -0
  189. package/dist/view/directives/index.d.ts.map +1 -0
  190. package/dist/view/directives/model.d.ts +7 -0
  191. package/dist/view/directives/model.d.ts.map +1 -0
  192. package/dist/view/directives/on.d.ts +7 -0
  193. package/dist/view/directives/on.d.ts.map +1 -0
  194. package/dist/view/directives/ref.d.ts +7 -0
  195. package/dist/view/directives/ref.d.ts.map +1 -0
  196. package/dist/view/directives/show.d.ts +7 -0
  197. package/dist/view/directives/show.d.ts.map +1 -0
  198. package/dist/view/directives/style.d.ts +7 -0
  199. package/dist/view/directives/style.d.ts.map +1 -0
  200. package/dist/view/directives/text.d.ts +7 -0
  201. package/dist/view/directives/text.d.ts.map +1 -0
  202. package/dist/view/evaluate.d.ts +43 -0
  203. package/dist/view/evaluate.d.ts.map +1 -0
  204. package/dist/view/index.d.ts +111 -0
  205. package/dist/view/index.d.ts.map +1 -0
  206. package/dist/view/mount.d.ts +69 -0
  207. package/dist/view/mount.d.ts.map +1 -0
  208. package/dist/view/process.d.ts +26 -0
  209. package/dist/view/process.d.ts.map +1 -0
  210. package/dist/view/types.d.ts +36 -0
  211. package/dist/view/types.d.ts.map +1 -0
  212. package/dist/view.es.mjs +426 -0
  213. package/dist/view.es.mjs.map +1 -0
  214. package/dist/watch-CXyaBC_9.js +58 -0
  215. package/dist/watch-CXyaBC_9.js.map +1 -0
  216. package/package.json +26 -14
  217. package/src/component/component.ts +289 -0
  218. package/src/component/html.ts +53 -0
  219. package/src/component/index.ts +40 -414
  220. package/src/component/props.ts +116 -0
  221. package/src/component/types.ts +85 -0
  222. package/src/core/collection.ts +588 -454
  223. package/src/core/dom.ts +38 -0
  224. package/src/core/element.ts +746 -740
  225. package/src/core/index.ts +43 -0
  226. package/src/core/utils/array.ts +102 -0
  227. package/src/core/utils/function.ts +110 -0
  228. package/src/core/utils/index.ts +83 -0
  229. package/src/core/utils/misc.ts +82 -0
  230. package/src/core/utils/number.ts +78 -0
  231. package/src/core/utils/object.ts +206 -0
  232. package/src/core/utils/string.ts +112 -0
  233. package/src/core/utils/type-guards.ts +112 -0
  234. package/src/full.ts +187 -106
  235. package/src/index.ts +36 -27
  236. package/src/motion/animate.ts +113 -0
  237. package/src/motion/easing.ts +40 -0
  238. package/src/motion/flip.ts +176 -0
  239. package/src/motion/index.ts +41 -358
  240. package/src/motion/keyframes.ts +46 -0
  241. package/src/motion/reduced-motion.ts +17 -0
  242. package/src/motion/scroll.ts +57 -0
  243. package/src/motion/spring.ts +150 -0
  244. package/src/motion/stagger.ts +43 -0
  245. package/src/motion/timeline.ts +246 -0
  246. package/src/motion/transition.ts +51 -0
  247. package/src/motion/types.ts +198 -0
  248. package/src/reactive/batch.ts +22 -0
  249. package/src/reactive/computed.ts +92 -0
  250. package/src/reactive/core.ts +93 -0
  251. package/src/reactive/effect.ts +43 -0
  252. package/src/reactive/index.ts +23 -22
  253. package/src/reactive/internals.ts +105 -0
  254. package/src/reactive/linked.ts +56 -0
  255. package/src/reactive/persisted.ts +74 -0
  256. package/src/reactive/readonly.ts +35 -0
  257. package/src/reactive/signal.ts +20 -506
  258. package/src/reactive/type-guards.ts +22 -0
  259. package/src/reactive/untrack.ts +31 -0
  260. package/src/reactive/watch.ts +73 -0
  261. package/src/router/index.ts +41 -0
  262. package/src/router/links.ts +130 -0
  263. package/src/router/match.ts +106 -0
  264. package/src/router/navigation.ts +71 -0
  265. package/src/router/query.ts +35 -0
  266. package/src/router/router.ts +211 -0
  267. package/src/router/state.ts +46 -0
  268. package/src/router/types.ts +93 -0
  269. package/src/router/utils.ts +116 -0
  270. package/src/security/constants.ts +209 -0
  271. package/src/security/csp.ts +77 -0
  272. package/src/security/index.ts +4 -12
  273. package/src/security/sanitize-core.ts +343 -0
  274. package/src/security/sanitize.ts +66 -625
  275. package/src/security/trusted-types.ts +69 -0
  276. package/src/security/types.ts +40 -0
  277. package/src/store/create-store.ts +329 -0
  278. package/src/store/define-store.ts +48 -0
  279. package/src/store/devtools.ts +45 -0
  280. package/src/store/index.ts +22 -0
  281. package/src/store/mapping.ts +73 -0
  282. package/src/store/persisted.ts +61 -0
  283. package/src/store/plugins.ts +32 -0
  284. package/src/store/registry.ts +51 -0
  285. package/src/store/types.ts +94 -0
  286. package/src/store/utils.ts +141 -0
  287. package/src/store/watch.ts +52 -0
  288. package/src/view/directives/bind.ts +23 -0
  289. package/src/view/directives/class.ts +70 -0
  290. package/src/view/directives/for.ts +275 -0
  291. package/src/view/directives/html.ts +19 -0
  292. package/src/view/directives/if.ts +30 -0
  293. package/src/view/directives/index.ts +11 -0
  294. package/src/view/directives/model.ts +56 -0
  295. package/src/view/directives/on.ts +41 -0
  296. package/src/view/directives/ref.ts +41 -0
  297. package/src/view/directives/show.ts +26 -0
  298. package/src/view/directives/style.ts +47 -0
  299. package/src/view/directives/text.ts +15 -0
  300. package/src/view/evaluate.ts +274 -0
  301. package/src/view/index.ts +112 -0
  302. package/src/view/mount.ts +200 -0
  303. package/src/view/process.ts +92 -0
  304. package/src/view/types.ts +44 -0
  305. package/dist/core/utils.d.ts +0 -313
  306. package/dist/core/utils.d.ts.map +0 -1
  307. package/src/core/utils.ts +0 -444
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Scroll-triggered animation helpers.
3
+ *
4
+ * @module bquery/motion
5
+ */
6
+
7
+ import { animate } from './animate';
8
+ import type { ScrollAnimateCleanup, ScrollAnimateOptions } from './types';
9
+
10
+ const resolveElements = (elements: Element | Iterable<Element> | ArrayLike<Element>): Element[] => {
11
+ if (typeof Element !== 'undefined' && elements instanceof Element) return [elements];
12
+ return Array.from(elements as Iterable<Element>);
13
+ };
14
+
15
+ /**
16
+ * Animate elements when they enter the viewport.
17
+ *
18
+ * @param elements - Target element(s)
19
+ * @param options - Scroll animation configuration
20
+ * @returns Cleanup function to disconnect observers
21
+ */
22
+ export const scrollAnimate = (
23
+ elements: Element | Iterable<Element> | ArrayLike<Element>,
24
+ options: ScrollAnimateOptions
25
+ ): ScrollAnimateCleanup => {
26
+ const targets = resolveElements(elements);
27
+ if (!targets.length) return () => undefined;
28
+
29
+ const { root = null, rootMargin, threshold, once = true, onEnter, ...animationConfig } = options;
30
+
31
+ if (typeof IntersectionObserver === 'undefined') {
32
+ targets.forEach((element) => {
33
+ onEnter?.(element);
34
+ void animate(element, animationConfig);
35
+ });
36
+ return () => undefined;
37
+ }
38
+
39
+ const observer = new IntersectionObserver(
40
+ (entries) => {
41
+ entries.forEach((entry) => {
42
+ if (!entry.isIntersecting) return;
43
+ const element = entry.target as Element;
44
+ onEnter?.(element);
45
+ void animate(element, animationConfig);
46
+ if (once) {
47
+ observer.unobserve(element);
48
+ }
49
+ });
50
+ },
51
+ { root, rootMargin, threshold }
52
+ );
53
+
54
+ targets.forEach((element) => observer.observe(element));
55
+
56
+ return () => observer.disconnect();
57
+ };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Spring physics helpers.
3
+ *
4
+ * @module bquery/motion
5
+ */
6
+
7
+ import type { Spring, SpringConfig } from './types';
8
+
9
+ /**
10
+ * Default spring configuration values.
11
+ */
12
+ const DEFAULT_SPRING_CONFIG: Required<SpringConfig> = {
13
+ stiffness: 100,
14
+ damping: 10,
15
+ mass: 1,
16
+ precision: 0.01,
17
+ };
18
+
19
+ /**
20
+ * Create a spring-based animation for smooth, physics-based motion.
21
+ *
22
+ * Uses variable frame rate timing based on `requestAnimationFrame` timestamps
23
+ * to ensure consistent animation speed across different devices and frame rates.
24
+ * Large time deltas (e.g., from tab backgrounding) are clamped to maintain
25
+ * simulation stability.
26
+ *
27
+ * @param initialValue - Starting value for the spring
28
+ * @param config - Spring physics configuration
29
+ * @returns Spring instance for controlling the animation
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const x = spring(0, { stiffness: 120, damping: 14 });
34
+ * x.onChange((value) => {
35
+ * element.style.transform = `translateX(${value}px)`;
36
+ * });
37
+ * await x.to(100);
38
+ * ```
39
+ */
40
+ export const spring = (initialValue: number, config: SpringConfig = {}): Spring => {
41
+ const { stiffness, damping, mass, precision } = {
42
+ ...DEFAULT_SPRING_CONFIG,
43
+ ...config,
44
+ };
45
+
46
+ let current = initialValue;
47
+ let velocity = 0;
48
+ let target = initialValue;
49
+ let animationFrame: number | null = null;
50
+ let resolvePromise: (() => void) | null = null;
51
+ let lastTime: number | null = null;
52
+ const listeners = new Set<(value: number) => void>();
53
+
54
+ const notifyListeners = () => {
55
+ for (const listener of listeners) {
56
+ listener(current);
57
+ }
58
+ };
59
+
60
+ const step = (timestamp: number) => {
61
+ // Calculate time delta (in seconds) from last frame
62
+ // If this is the first frame, use a sensible default (1/60s)
63
+ // This ensures the animation speed is independent of frame rate
64
+ const deltaTime = lastTime !== null ? (timestamp - lastTime) / 1000 : 1 / 60;
65
+ // Clamp large deltas to prevent instability (e.g. tab backgrounding)
66
+ // Maximum delta of 1/30s (~33ms) keeps simulation stable
67
+ const clampedDelta = Math.min(deltaTime, 1 / 30);
68
+ lastTime = timestamp;
69
+
70
+ // Spring physics calculation
71
+ const displacement = current - target;
72
+ const springForce = -stiffness * displacement;
73
+ const dampingForce = -damping * velocity;
74
+ const acceleration = (springForce + dampingForce) / mass;
75
+
76
+ velocity += acceleration * clampedDelta;
77
+ current += velocity * clampedDelta;
78
+
79
+ notifyListeners();
80
+
81
+ // Check if spring has settled
82
+ if (Math.abs(velocity) < precision && Math.abs(displacement) < precision) {
83
+ current = target;
84
+ velocity = 0;
85
+ animationFrame = null;
86
+ notifyListeners();
87
+ resolvePromise?.();
88
+ resolvePromise = null;
89
+ return;
90
+ }
91
+
92
+ animationFrame = requestAnimationFrame(step);
93
+ };
94
+
95
+ return {
96
+ to(newTarget: number): Promise<void> {
97
+ target = newTarget;
98
+
99
+ if (animationFrame !== null) {
100
+ cancelAnimationFrame(animationFrame);
101
+ }
102
+
103
+ // Resolve any pending promise from a previous to() call
104
+ // This ensures all returned promises eventually settle
105
+ resolvePromise?.();
106
+
107
+ // Reset lastTime to ensure clean start for new animation
108
+ lastTime = null;
109
+
110
+ return new Promise((resolve) => {
111
+ resolvePromise = resolve;
112
+ animationFrame = requestAnimationFrame(step);
113
+ });
114
+ },
115
+
116
+ current(): number {
117
+ return current;
118
+ },
119
+
120
+ stop(): void {
121
+ if (animationFrame !== null) {
122
+ cancelAnimationFrame(animationFrame);
123
+ animationFrame = null;
124
+ }
125
+ velocity = 0;
126
+ lastTime = null;
127
+ resolvePromise?.();
128
+ resolvePromise = null;
129
+ },
130
+
131
+ onChange(callback: (value: number) => void): () => void {
132
+ listeners.add(callback);
133
+ return () => listeners.delete(callback);
134
+ },
135
+ };
136
+ };
137
+
138
+ /**
139
+ * Preset spring configurations for common use cases.
140
+ */
141
+ export const springPresets = {
142
+ /** Gentle, slow-settling spring */
143
+ gentle: { stiffness: 80, damping: 15 } as SpringConfig,
144
+ /** Responsive, snappy spring */
145
+ snappy: { stiffness: 200, damping: 20 } as SpringConfig,
146
+ /** Bouncy, playful spring */
147
+ bouncy: { stiffness: 300, damping: 8 } as SpringConfig,
148
+ /** Stiff, quick spring with minimal overshoot */
149
+ stiff: { stiffness: 400, damping: 30 } as SpringConfig,
150
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Stagger helpers.
3
+ *
4
+ * @module bquery/motion
5
+ */
6
+
7
+ import type { StaggerFunction, StaggerOptions } from './types';
8
+
9
+ /**
10
+ * Create a staggered delay function for list animations.
11
+ *
12
+ * @param step - Delay between items in milliseconds
13
+ * @param options - Stagger configuration
14
+ * @returns Function that returns delay for a given index
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const delay = stagger(50, { from: 'center' });
19
+ * delay(0, 3); // 50
20
+ * delay(1, 3); // 0
21
+ * ```
22
+ */
23
+ export const stagger = (step: number, options: StaggerOptions = {}): StaggerFunction => {
24
+ const { start = 0, from = 'start', easing } = options;
25
+
26
+ return (index: number, total = 0): number => {
27
+ const origin =
28
+ typeof from === 'number'
29
+ ? from
30
+ : from === 'center'
31
+ ? (total - 1) / 2
32
+ : from === 'end'
33
+ ? total - 1
34
+ : 0;
35
+
36
+ const distance = Math.abs(index - origin);
37
+ const maxDistance = total > 1 ? Math.max(origin, total - 1 - origin) : 1;
38
+ const normalized = maxDistance === 0 ? 0 : distance / maxDistance;
39
+ const eased = easing ? easing(normalized) * maxDistance : distance;
40
+
41
+ return start + eased * step;
42
+ };
43
+ };
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Timeline and sequence helpers.
3
+ *
4
+ * @module bquery/motion
5
+ */
6
+
7
+ import { animate, applyFinalKeyframeStyles } from './animate';
8
+ import { prefersReducedMotion } from './reduced-motion';
9
+ import type {
10
+ SequenceOptions,
11
+ SequenceStep,
12
+ TimelineConfig,
13
+ TimelineControls,
14
+ TimelineStep,
15
+ } from './types';
16
+
17
+ const resolveTimeValue = (value?: number | string): number => {
18
+ if (typeof value === 'number') return value;
19
+ if (typeof value === 'string') {
20
+ const trimmed = value.trim();
21
+ if (trimmed.endsWith('ms')) {
22
+ const parsed = Number.parseFloat(trimmed.slice(0, -2));
23
+ return Number.isFinite(parsed) ? parsed : 0;
24
+ }
25
+ if (trimmed.endsWith('s')) {
26
+ const parsed = Number.parseFloat(trimmed.slice(0, -1));
27
+ return Number.isFinite(parsed) ? parsed * 1000 : 0;
28
+ }
29
+ const parsed = Number.parseFloat(trimmed);
30
+ return Number.isFinite(parsed) ? parsed : 0;
31
+ }
32
+ return 0;
33
+ };
34
+
35
+ const resolveAt = (at: TimelineStep['at'], previousEnd: number): number => {
36
+ if (typeof at === 'number') return at;
37
+ if (typeof at === 'string') {
38
+ const match = /^([+-])=(\d+(?:\.\d+)?)$/.exec(at);
39
+ if (match) {
40
+ const delta = Number.parseFloat(match[2]);
41
+ if (!Number.isFinite(delta)) return previousEnd;
42
+ return match[1] === '+' ? previousEnd + delta : previousEnd - delta;
43
+ }
44
+ }
45
+ return previousEnd;
46
+ };
47
+
48
+ const normalizeDuration = (options?: KeyframeAnimationOptions): number => {
49
+ const baseDuration = resolveTimeValue(options?.duration as number | string | undefined);
50
+ const endDelay = resolveTimeValue(options?.endDelay as number | string | undefined);
51
+ const rawIterations = options?.iterations ?? 1;
52
+
53
+ // Handle infinite iterations - treat as a special case with a very large duration
54
+ // In practice, infinite iterations shouldn't be used in timelines as they never end
55
+ if (rawIterations === Infinity) {
56
+ // Return a large sentinel value - timeline calculations will be incorrect,
57
+ // but this at least prevents NaN/Infinity from breaking scheduling
58
+ return Number.MAX_SAFE_INTEGER;
59
+ }
60
+
61
+ // Per Web Animations spec, iterations must be a non-negative number
62
+ // Treat negative as 0 (only endDelay duration)
63
+ const iterations = Math.max(0, rawIterations);
64
+
65
+ // Total duration = (baseDuration * iterations) + endDelay
66
+ // Note: endDelay is applied once at the end, after all iterations
67
+ return baseDuration * iterations + endDelay;
68
+ };
69
+
70
+ const scheduleSteps = (steps: TimelineStep[]) => {
71
+ let previousEnd = 0;
72
+ return steps.map((step) => {
73
+ const baseStart = resolveAt(step.at, previousEnd);
74
+ const stepDelay = resolveTimeValue(step.options?.delay as number | string | undefined);
75
+ const start = Math.max(0, baseStart + stepDelay);
76
+ const duration = normalizeDuration(step.options);
77
+ const end = start + duration;
78
+ previousEnd = Math.max(previousEnd, end);
79
+ return { step, start, end, duration };
80
+ });
81
+ };
82
+
83
+ /**
84
+ * Run a list of animations sequentially.
85
+ *
86
+ * @param steps - Steps to run in order
87
+ * @param options - Sequence configuration
88
+ */
89
+ export const sequence = async (
90
+ steps: SequenceStep[],
91
+ options: SequenceOptions = {}
92
+ ): Promise<void> => {
93
+ const { stagger, onFinish } = options;
94
+ const total = steps.length;
95
+
96
+ for (let index = 0; index < steps.length; index += 1) {
97
+ const step = steps[index];
98
+ const delay = stagger ? stagger(index, total) : 0;
99
+ if (delay > 0) {
100
+ await new Promise((resolve) => setTimeout(resolve, delay));
101
+ }
102
+ await animate(step.target, step);
103
+ }
104
+
105
+ onFinish?.();
106
+ };
107
+
108
+ /**
109
+ * Create a timeline controller for multiple animations.
110
+ *
111
+ * @param initialSteps - Steps for the timeline
112
+ * @param config - Timeline configuration
113
+ */
114
+ export const timeline = (
115
+ initialSteps: TimelineStep[] = [],
116
+ config: TimelineConfig = {}
117
+ ): TimelineControls => {
118
+ const steps = [...initialSteps];
119
+ const listeners = new Set<() => void>();
120
+ let animations: Array<{ animation: Animation; step: TimelineStep; start: number }> = [];
121
+ let totalDuration = 0;
122
+ let reducedMotionApplied = false;
123
+ let finalized = false;
124
+
125
+ const { commitStyles = true, respectReducedMotion = true, onFinish } = config;
126
+
127
+ const finalize = () => {
128
+ if (finalized) return;
129
+ finalized = true;
130
+
131
+ if (commitStyles) {
132
+ for (const item of animations) {
133
+ const { animation, step } = item;
134
+ if (typeof animation.commitStyles === 'function') {
135
+ animation.commitStyles();
136
+ } else {
137
+ applyFinalKeyframeStyles(step.target, step.keyframes);
138
+ }
139
+ animation.cancel();
140
+ }
141
+ }
142
+
143
+ listeners.forEach((listener) => listener());
144
+ onFinish?.();
145
+ };
146
+
147
+ const buildAnimations = () => {
148
+ animations.forEach(({ animation }) => animation.cancel());
149
+ animations = [];
150
+ finalized = false;
151
+
152
+ const schedule = scheduleSteps(steps);
153
+ totalDuration = schedule.length ? Math.max(...schedule.map((item) => item.end)) : 0;
154
+
155
+ if (respectReducedMotion && prefersReducedMotion()) {
156
+ if (commitStyles) {
157
+ schedule.forEach(({ step }) => applyFinalKeyframeStyles(step.target, step.keyframes));
158
+ }
159
+ reducedMotionApplied = true;
160
+ return;
161
+ }
162
+
163
+ // Check if Web Animations API is available on all targets
164
+ const animateUnavailable = schedule.some(
165
+ ({ step }) => typeof (step.target as HTMLElement).animate !== 'function'
166
+ );
167
+ if (animateUnavailable) {
168
+ if (commitStyles) {
169
+ schedule.forEach(({ step }) => applyFinalKeyframeStyles(step.target, step.keyframes));
170
+ }
171
+ reducedMotionApplied = true;
172
+ return;
173
+ }
174
+
175
+ reducedMotionApplied = false;
176
+ animations = schedule.map(({ step, start }) => {
177
+ const { delay: _delay, ...options } = step.options ?? {};
178
+ const animation = step.target.animate(step.keyframes, {
179
+ ...options,
180
+ delay: start,
181
+ fill: options.fill ?? 'both',
182
+ });
183
+ return { animation, step, start };
184
+ });
185
+ };
186
+
187
+ return {
188
+ add(step: TimelineStep): void {
189
+ steps.push(step);
190
+ },
191
+
192
+ duration(): number {
193
+ if (!steps.length) return 0;
194
+ if (!animations.length) {
195
+ const schedule = scheduleSteps(steps);
196
+ return Math.max(...schedule.map((item) => item.end));
197
+ }
198
+ return totalDuration;
199
+ },
200
+
201
+ async play(): Promise<void> {
202
+ buildAnimations();
203
+
204
+ if (reducedMotionApplied || animations.length === 0) {
205
+ finalize();
206
+ return;
207
+ }
208
+
209
+ const finishPromises = animations.map((item) =>
210
+ item.animation.finished.catch(() => undefined)
211
+ );
212
+ await Promise.all(finishPromises);
213
+ finalize();
214
+ },
215
+
216
+ pause(): void {
217
+ if (reducedMotionApplied) return;
218
+ animations.forEach(({ animation }) => animation.pause());
219
+ },
220
+
221
+ resume(): void {
222
+ if (reducedMotionApplied) return;
223
+ animations.forEach(({ animation }) => animation.play());
224
+ },
225
+
226
+ stop(): void {
227
+ animations.forEach(({ animation }) => animation.cancel());
228
+ animations = [];
229
+ reducedMotionApplied = false;
230
+ },
231
+
232
+ seek(time: number): void {
233
+ if (reducedMotionApplied) return;
234
+ animations.forEach(({ animation }) => {
235
+ // currentTime is measured from the beginning of the animation including delay,
236
+ // so we set it directly to the requested timeline time
237
+ animation.currentTime = time;
238
+ });
239
+ },
240
+
241
+ onFinish(callback: () => void): () => void {
242
+ listeners.add(callback);
243
+ return () => listeners.delete(callback);
244
+ },
245
+ };
246
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * View transition helpers.
3
+ *
4
+ * @module bquery/motion
5
+ */
6
+
7
+ import type { TransitionOptions } from './types';
8
+
9
+ /** Extended document type with View Transitions API */
10
+ type DocumentWithTransition = Document & {
11
+ startViewTransition?: (callback: () => void) => {
12
+ finished: Promise<void>;
13
+ ready: Promise<void>;
14
+ updateCallbackDone: Promise<void>;
15
+ };
16
+ };
17
+
18
+ /**
19
+ * Execute a DOM update with view transition animation.
20
+ * Falls back to immediate update when View Transitions API is unavailable.
21
+ *
22
+ * @param updateOrOptions - Update function or options object
23
+ * @returns Promise that resolves when transition completes
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * await transition(() => {
28
+ * $('#content').text('Updated');
29
+ * });
30
+ * ```
31
+ */
32
+ export const transition = async (
33
+ updateOrOptions: (() => void) | TransitionOptions
34
+ ): Promise<void> => {
35
+ const update = typeof updateOrOptions === 'function' ? updateOrOptions : updateOrOptions.update;
36
+
37
+ // SSR/non-DOM environment fallback
38
+ if (typeof document === 'undefined') {
39
+ update();
40
+ return;
41
+ }
42
+
43
+ const doc = document as DocumentWithTransition;
44
+
45
+ if (doc.startViewTransition) {
46
+ await doc.startViewTransition(() => update()).finished;
47
+ return;
48
+ }
49
+
50
+ update();
51
+ };