@bquery/bquery 1.4.0 → 1.5.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.
- package/README.md +139 -120
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/index.d.ts +2 -0
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/library.d.ts +34 -0
- package/dist/component/library.d.ts.map +1 -0
- package/dist/component/types.d.ts +10 -6
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component-CY5MVoYN.js +531 -0
- package/dist/component-CY5MVoYN.js.map +1 -0
- package/dist/component.es.mjs +6 -184
- package/dist/config-DRmZZno3.js +40 -0
- package/dist/config-DRmZZno3.js.map +1 -0
- package/dist/core-CK2Mfpf4.js +648 -0
- package/dist/core-CK2Mfpf4.js.map +1 -0
- package/dist/core-DPdbItcq.js +112 -0
- package/dist/core-DPdbItcq.js.map +1 -0
- package/dist/core.es.mjs +45 -1261
- package/dist/full.d.ts +6 -6
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +98 -92
- package/dist/full.iife.js +173 -3
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +173 -3
- package/dist/full.umd.js.map +1 -1
- package/dist/index.es.mjs +143 -139
- package/dist/motion/transition.d.ts +1 -1
- package/dist/motion/transition.d.ts.map +1 -1
- package/dist/motion/types.d.ts +11 -1
- package/dist/motion/types.d.ts.map +1 -1
- package/dist/motion-C5DRdPnO.js +415 -0
- package/dist/motion-C5DRdPnO.js.map +1 -0
- package/dist/motion.es.mjs +25 -361
- package/dist/object-qGpWr6-J.js +38 -0
- package/dist/object-qGpWr6-J.js.map +1 -0
- package/dist/platform/announcer.d.ts +59 -0
- package/dist/platform/announcer.d.ts.map +1 -0
- package/dist/platform/config.d.ts +92 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/cookies.d.ts +45 -0
- package/dist/platform/cookies.d.ts.map +1 -0
- package/dist/platform/index.d.ts +8 -0
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/meta.d.ts +62 -0
- package/dist/platform/meta.d.ts.map +1 -0
- package/dist/platform-B7JhGBc7.js +361 -0
- package/dist/platform-B7JhGBc7.js.map +1 -0
- package/dist/platform.es.mjs +11 -248
- package/dist/reactive/async-data.d.ts +114 -0
- package/dist/reactive/async-data.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/signal.d.ts +2 -0
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive-BDya-ia8.js +253 -0
- package/dist/reactive-BDya-ia8.js.map +1 -0
- package/dist/reactive.es.mjs +18 -34
- package/dist/router-CijiICxt.js +188 -0
- package/dist/router-CijiICxt.js.map +1 -0
- package/dist/router.es.mjs +11 -200
- package/dist/sanitize-jyJ2ryE2.js +302 -0
- package/dist/sanitize-jyJ2ryE2.js.map +1 -0
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security.es.mjs +10 -56
- package/dist/store-CPK9E62U.js +262 -0
- package/dist/store-CPK9E62U.js.map +1 -0
- package/dist/store.es.mjs +12 -25
- package/dist/view-Cdi0g-qo.js +396 -0
- package/dist/view-Cdi0g-qo.js.map +1 -0
- package/dist/view.es.mjs +10 -430
- package/package.json +15 -11
- package/src/component/component.ts +319 -289
- package/src/component/index.ts +42 -40
- package/src/component/library.ts +504 -0
- package/src/component/types.ts +91 -85
- package/src/core/collection.ts +628 -628
- package/src/core/element.ts +774 -774
- package/src/core/index.ts +48 -48
- package/src/core/utils/function.ts +151 -151
- package/src/full.ts +223 -187
- package/src/motion/animate.ts +113 -113
- package/src/motion/flip.ts +176 -176
- package/src/motion/scroll.ts +57 -57
- package/src/motion/spring.ts +150 -150
- package/src/motion/timeline.ts +246 -246
- package/src/motion/transition.ts +53 -7
- package/src/motion/types.ts +208 -198
- package/src/platform/announcer.ts +208 -0
- package/src/platform/config.ts +163 -0
- package/src/platform/cookies.ts +165 -0
- package/src/platform/index.ts +39 -18
- package/src/platform/meta.ts +168 -0
- package/src/platform/storage.ts +215 -215
- package/src/reactive/async-data.ts +486 -0
- package/src/reactive/core.ts +114 -114
- package/src/reactive/effect.ts +54 -54
- package/src/reactive/index.ts +37 -23
- package/src/reactive/internals.ts +122 -122
- package/src/reactive/signal.ts +29 -20
- package/src/security/constants.ts +211 -209
- package/src/security/sanitize-core.ts +364 -364
- package/src/view/evaluate.ts +290 -290
- package/dist/batch-x7b2eZST.js +0 -13
- package/dist/batch-x7b2eZST.js.map +0 -1
- package/dist/component.es.mjs.map +0 -1
- package/dist/core-BhpuvPhy.js +0 -170
- package/dist/core-BhpuvPhy.js.map +0 -1
- package/dist/core.es.mjs.map +0 -1
- package/dist/full.es.mjs.map +0 -1
- package/dist/index.es.mjs.map +0 -1
- package/dist/motion.es.mjs.map +0 -1
- package/dist/persisted-DHoi3uEs.js +0 -278
- package/dist/persisted-DHoi3uEs.js.map +0 -1
- package/dist/platform.es.mjs.map +0 -1
- package/dist/reactive.es.mjs.map +0 -1
- package/dist/router.es.mjs.map +0 -1
- package/dist/sanitize-Cxvxa-DX.js +0 -283
- package/dist/sanitize-Cxvxa-DX.js.map +0 -1
- package/dist/security.es.mjs.map +0 -1
- package/dist/store.es.mjs.map +0 -1
- package/dist/type-guards-BdKlYYlS.js +0 -32
- package/dist/type-guards-BdKlYYlS.js.map +0 -1
- package/dist/untrack-DNnnqdlR.js +0 -6
- package/dist/untrack-DNnnqdlR.js.map +0 -1
- package/dist/view.es.mjs.map +0 -1
- package/dist/watch-DXXv3iAI.js +0 -58
- package/dist/watch-DXXv3iAI.js.map +0 -1
package/src/motion/timeline.ts
CHANGED
|
@@ -1,246 +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
|
-
};
|
|
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
|
+
};
|
package/src/motion/transition.ts
CHANGED
|
@@ -5,16 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TransitionOptions } from './types';
|
|
8
|
+
import { prefersReducedMotion } from './reduced-motion';
|
|
9
|
+
import { getBqueryConfig } from '../platform/config';
|
|
8
10
|
|
|
9
11
|
/** Extended document type with View Transitions API */
|
|
10
12
|
type DocumentWithTransition = Document & {
|
|
11
|
-
startViewTransition?: (callback: () => void) => {
|
|
13
|
+
startViewTransition?: (callback: () => void | Promise<void>) => {
|
|
12
14
|
finished: Promise<void>;
|
|
13
15
|
ready: Promise<void>;
|
|
14
16
|
updateCallbackDone: Promise<void>;
|
|
17
|
+
skipTransition?: () => void;
|
|
18
|
+
types?: {
|
|
19
|
+
add: (type: string) => void;
|
|
20
|
+
};
|
|
15
21
|
};
|
|
16
22
|
};
|
|
17
23
|
|
|
24
|
+
const sanitizeTokens = (tokens?: string[]): string[] =>
|
|
25
|
+
(tokens ?? []).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* Execute a DOM update with view transition animation.
|
|
20
29
|
* Falls back to immediate update when View Transitions API is unavailable.
|
|
@@ -30,22 +39,59 @@ type DocumentWithTransition = Document & {
|
|
|
30
39
|
* ```
|
|
31
40
|
*/
|
|
32
41
|
export const transition = async (
|
|
33
|
-
updateOrOptions: (() => void) | TransitionOptions
|
|
42
|
+
updateOrOptions: (() => void | Promise<void>) | TransitionOptions
|
|
34
43
|
): Promise<void> => {
|
|
35
|
-
const
|
|
44
|
+
const config = getBqueryConfig().transitions;
|
|
45
|
+
const options: TransitionOptions =
|
|
46
|
+
typeof updateOrOptions === 'function'
|
|
47
|
+
? {
|
|
48
|
+
update: updateOrOptions,
|
|
49
|
+
classes: config?.classes,
|
|
50
|
+
types: config?.types,
|
|
51
|
+
skipOnReducedMotion: config?.skipOnReducedMotion,
|
|
52
|
+
}
|
|
53
|
+
: {
|
|
54
|
+
...updateOrOptions,
|
|
55
|
+
classes: updateOrOptions.classes ?? config?.classes,
|
|
56
|
+
types: updateOrOptions.types ?? config?.types,
|
|
57
|
+
skipOnReducedMotion: updateOrOptions.skipOnReducedMotion ?? config?.skipOnReducedMotion,
|
|
58
|
+
};
|
|
59
|
+
const update = options.update;
|
|
36
60
|
|
|
37
61
|
// SSR/non-DOM environment fallback
|
|
38
62
|
if (typeof document === 'undefined') {
|
|
39
|
-
update();
|
|
63
|
+
await update();
|
|
40
64
|
return;
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
const doc = document as DocumentWithTransition;
|
|
68
|
+
const root = document.documentElement;
|
|
69
|
+
const classes = sanitizeTokens(options.classes);
|
|
70
|
+
const types = sanitizeTokens(options.types);
|
|
44
71
|
|
|
45
|
-
if (doc.startViewTransition) {
|
|
46
|
-
await
|
|
72
|
+
if (!doc.startViewTransition || (options.skipOnReducedMotion && prefersReducedMotion())) {
|
|
73
|
+
await update();
|
|
74
|
+
options.onFinish?.();
|
|
47
75
|
return;
|
|
48
76
|
}
|
|
49
77
|
|
|
50
|
-
|
|
78
|
+
classes.forEach((className: string) => root.classList.add(className));
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const viewTransition = doc.startViewTransition(() => update());
|
|
82
|
+
const transitionTypes = viewTransition.types;
|
|
83
|
+
|
|
84
|
+
if (transitionTypes) {
|
|
85
|
+
for (const type of types) {
|
|
86
|
+
transitionTypes.add(type);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await viewTransition.ready;
|
|
91
|
+
options.onReady?.();
|
|
92
|
+
await viewTransition.finished;
|
|
93
|
+
options.onFinish?.();
|
|
94
|
+
} finally {
|
|
95
|
+
classes.forEach((className: string) => root.classList.remove(className));
|
|
96
|
+
}
|
|
51
97
|
};
|