pulse-js-framework 1.9.3 → 1.10.1
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/compiler/parser/_extract.js +393 -0
- package/compiler/parser/blocks.js +361 -0
- package/compiler/parser/core.js +306 -0
- package/compiler/parser/expressions.js +386 -0
- package/compiler/parser/imports.js +108 -0
- package/compiler/parser/index.js +47 -0
- package/compiler/parser/state.js +155 -0
- package/compiler/parser/style.js +445 -0
- package/compiler/parser/view.js +632 -0
- package/compiler/parser.js +15 -2372
- package/compiler/parser.js.original +2376 -0
- package/package.json +29 -1
- package/runtime/a11y/announcements.js +213 -0
- package/runtime/a11y/contrast.js +125 -0
- package/runtime/a11y/focus.js +412 -0
- package/runtime/a11y/index.js +35 -0
- package/runtime/a11y/preferences.js +121 -0
- package/runtime/a11y/utils.js +164 -0
- package/runtime/a11y/validation.js +258 -0
- package/runtime/a11y/widgets.js +545 -0
- package/runtime/a11y.js +15 -1840
- package/runtime/a11y.js.original +1844 -0
- package/runtime/animation.js +535 -0
- package/runtime/dom-advanced.js +116 -37
- package/runtime/graphql/cache.js +69 -0
- package/runtime/graphql/client.js +563 -0
- package/runtime/graphql/hooks.js +492 -0
- package/runtime/graphql/index.js +62 -0
- package/runtime/graphql/subscriptions.js +241 -0
- package/runtime/graphql.js +12 -1322
- package/runtime/graphql.js.original +1326 -0
- package/runtime/i18n.js +434 -0
- package/runtime/index.js +20 -0
- package/runtime/logger.js +5 -1
- package/runtime/persistence.js +492 -0
- package/runtime/router/core.js +956 -0
- package/runtime/router/guards.js +90 -0
- package/runtime/router/history.js +204 -0
- package/runtime/router/index.js +36 -0
- package/runtime/router/lazy.js +180 -0
- package/runtime/router/utils.js +226 -0
- package/runtime/router.js +12 -1600
- package/runtime/router.js.original +1605 -0
- package/runtime/sse.js +393 -0
- package/runtime/sw.js +250 -0
- package/sw/index.js +240 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Animation Module
|
|
3
|
+
* Web Animations API wrapper with Pulse reactivity and reduced motion support.
|
|
4
|
+
*
|
|
5
|
+
* @module pulse-js-framework/runtime/animation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pulse, computed, effect, onCleanup } from './pulse.js';
|
|
9
|
+
import { loggers } from './logger.js';
|
|
10
|
+
import { getAdapter, MockDOMAdapter } from './dom-adapter.js';
|
|
11
|
+
import { prefersReducedMotion } from './a11y.js';
|
|
12
|
+
|
|
13
|
+
const log = loggers.dom;
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// CONFIGURATION
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
const _config = {
|
|
20
|
+
respectReducedMotion: true,
|
|
21
|
+
defaultDuration: 300,
|
|
22
|
+
defaultEasing: 'ease-out',
|
|
23
|
+
disabled: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configure global animation settings
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
* @param {boolean} [options.respectReducedMotion=true] - Respect prefers-reduced-motion
|
|
31
|
+
* @param {number} [options.defaultDuration=300] - Default animation duration (ms)
|
|
32
|
+
* @param {string} [options.defaultEasing='ease-out'] - Default easing function
|
|
33
|
+
* @param {boolean} [options.disabled=false] - Kill switch to disable all animations
|
|
34
|
+
*/
|
|
35
|
+
export function configureAnimations(options = {}) {
|
|
36
|
+
if (options.respectReducedMotion !== undefined) _config.respectReducedMotion = options.respectReducedMotion;
|
|
37
|
+
if (options.defaultDuration !== undefined) _config.defaultDuration = options.defaultDuration;
|
|
38
|
+
if (options.defaultEasing !== undefined) _config.defaultEasing = options.defaultEasing;
|
|
39
|
+
if (options.disabled !== undefined) _config.disabled = options.disabled;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// INTERNAL HELPERS
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
function _isSSR() {
|
|
47
|
+
const adapter = getAdapter();
|
|
48
|
+
return adapter instanceof MockDOMAdapter;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _shouldAnimate() {
|
|
52
|
+
if (_config.disabled) return false;
|
|
53
|
+
if (_isSSR()) return false;
|
|
54
|
+
if (_config.respectReducedMotion && prefersReducedMotion()) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _getEffectiveDuration(duration) {
|
|
59
|
+
if (!_shouldAnimate()) return 0;
|
|
60
|
+
return duration ?? _config.defaultDuration;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// animate()
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Animate an element using the Web Animations API
|
|
69
|
+
*
|
|
70
|
+
* @param {HTMLElement} element - Element to animate
|
|
71
|
+
* @param {Array<Object>|Object} keyframes - Keyframes (WAAPI format)
|
|
72
|
+
* @param {Object} [options] - Animation options
|
|
73
|
+
* @param {number} [options.duration] - Duration in ms (default: configureAnimations default)
|
|
74
|
+
* @param {string} [options.easing] - Easing function
|
|
75
|
+
* @param {string} [options.fill='none'] - Fill mode
|
|
76
|
+
* @param {number} [options.delay=0] - Delay in ms
|
|
77
|
+
* @param {number} [options.iterations=1] - Number of iterations
|
|
78
|
+
* @param {string} [options.direction='normal'] - Direction
|
|
79
|
+
* @returns {Object} AnimationControl with reactive state
|
|
80
|
+
*/
|
|
81
|
+
export function animate(element, keyframes, options = {}) {
|
|
82
|
+
const isPlaying = pulse(false);
|
|
83
|
+
const progress = pulse(0);
|
|
84
|
+
|
|
85
|
+
// SSR or disabled: return no-op control
|
|
86
|
+
if (!_shouldAnimate() || !element || typeof element.animate !== 'function') {
|
|
87
|
+
return _createNoopControl(isPlaying, progress);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const duration = _getEffectiveDuration(options.duration);
|
|
91
|
+
const animationOptions = {
|
|
92
|
+
duration,
|
|
93
|
+
easing: options.easing ?? _config.defaultEasing,
|
|
94
|
+
fill: options.fill ?? 'none',
|
|
95
|
+
delay: options.delay ?? 0,
|
|
96
|
+
iterations: options.iterations ?? 1,
|
|
97
|
+
direction: options.direction ?? 'normal',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let animation = null;
|
|
101
|
+
let rafId = null;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
animation = element.animate(keyframes, animationOptions);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
log.warn('Animation failed:', e.message);
|
|
107
|
+
return _createNoopControl(isPlaying, progress);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Track playing state
|
|
111
|
+
isPlaying.set(true);
|
|
112
|
+
|
|
113
|
+
function _updateProgress() {
|
|
114
|
+
if (!animation) return;
|
|
115
|
+
const timing = animation.effect?.getComputedTiming?.();
|
|
116
|
+
if (timing) {
|
|
117
|
+
progress.set(typeof timing.progress === 'number' ? timing.progress : 0);
|
|
118
|
+
}
|
|
119
|
+
if (animation.playState === 'running') {
|
|
120
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
125
|
+
|
|
126
|
+
const finishedPromise = animation.finished
|
|
127
|
+
.then(() => {
|
|
128
|
+
isPlaying.set(false);
|
|
129
|
+
progress.set(1);
|
|
130
|
+
})
|
|
131
|
+
.catch(() => {
|
|
132
|
+
// Animation was cancelled
|
|
133
|
+
isPlaying.set(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
animation.addEventListener('cancel', () => {
|
|
137
|
+
isPlaying.set(false);
|
|
138
|
+
if (rafId) {
|
|
139
|
+
cancelAnimationFrame(rafId);
|
|
140
|
+
rafId = null;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function _cleanup() {
|
|
145
|
+
if (rafId) {
|
|
146
|
+
cancelAnimationFrame(rafId);
|
|
147
|
+
rafId = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
isPlaying,
|
|
153
|
+
progress,
|
|
154
|
+
finished: finishedPromise,
|
|
155
|
+
|
|
156
|
+
play() {
|
|
157
|
+
if (animation) {
|
|
158
|
+
animation.play();
|
|
159
|
+
isPlaying.set(true);
|
|
160
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
pause() {
|
|
165
|
+
if (animation) {
|
|
166
|
+
animation.pause();
|
|
167
|
+
isPlaying.set(false);
|
|
168
|
+
_cleanup();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
reverse() {
|
|
173
|
+
if (animation) {
|
|
174
|
+
animation.reverse();
|
|
175
|
+
isPlaying.set(true);
|
|
176
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
cancel() {
|
|
181
|
+
if (animation) {
|
|
182
|
+
animation.cancel();
|
|
183
|
+
isPlaying.set(false);
|
|
184
|
+
progress.set(0);
|
|
185
|
+
_cleanup();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
finish() {
|
|
190
|
+
if (animation) {
|
|
191
|
+
animation.finish();
|
|
192
|
+
isPlaying.set(false);
|
|
193
|
+
progress.set(1);
|
|
194
|
+
_cleanup();
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
dispose() {
|
|
199
|
+
_cleanup();
|
|
200
|
+
if (animation) {
|
|
201
|
+
try { animation.cancel(); } catch { /* already finished */ }
|
|
202
|
+
}
|
|
203
|
+
isPlaying.set(false);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _createNoopControl(isPlaying, progress) {
|
|
209
|
+
return {
|
|
210
|
+
isPlaying,
|
|
211
|
+
progress,
|
|
212
|
+
finished: Promise.resolve(),
|
|
213
|
+
play() {},
|
|
214
|
+
pause() {},
|
|
215
|
+
reverse() {},
|
|
216
|
+
cancel() {},
|
|
217
|
+
finish() { progress.set(1); },
|
|
218
|
+
dispose() {},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// useTransition()
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Reactive enter/leave transition hook
|
|
228
|
+
*
|
|
229
|
+
* @param {Function} condition - Reactive condition function
|
|
230
|
+
* @param {Object} options - Transition options
|
|
231
|
+
* @param {Object} [options.enter] - Enter keyframes
|
|
232
|
+
* @param {Object} [options.leave] - Leave keyframes
|
|
233
|
+
* @param {number} [options.duration] - Duration in ms
|
|
234
|
+
* @param {string} [options.easing] - Easing function
|
|
235
|
+
* @param {Function} [options.onEnter] - Template factory for entering content
|
|
236
|
+
* @param {Function} [options.onLeave] - Template factory for leaving content (optional)
|
|
237
|
+
* @returns {Object} { nodes, isEntering, isLeaving }
|
|
238
|
+
*/
|
|
239
|
+
export function useTransition(condition, options = {}) {
|
|
240
|
+
const dom = getAdapter();
|
|
241
|
+
|
|
242
|
+
const {
|
|
243
|
+
enter = { opacity: [0, 1] },
|
|
244
|
+
leave = { opacity: [1, 0] },
|
|
245
|
+
duration,
|
|
246
|
+
easing,
|
|
247
|
+
onEnter = null,
|
|
248
|
+
onLeave = null,
|
|
249
|
+
} = options;
|
|
250
|
+
|
|
251
|
+
const isEntering = pulse(false);
|
|
252
|
+
const isLeaving = pulse(false);
|
|
253
|
+
|
|
254
|
+
const container = dom.createDocumentFragment();
|
|
255
|
+
const marker = dom.createComment('transition');
|
|
256
|
+
dom.appendChild(container, marker);
|
|
257
|
+
|
|
258
|
+
let currentNodes = [];
|
|
259
|
+
let activeAnimation = null;
|
|
260
|
+
|
|
261
|
+
function _removeCurrentNodes() {
|
|
262
|
+
for (const node of currentNodes) {
|
|
263
|
+
dom.removeNode(node);
|
|
264
|
+
if (node._pulseUnmount) {
|
|
265
|
+
for (const cb of node._pulseUnmount) cb();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
currentNodes = [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function _insertNodes(nodes) {
|
|
272
|
+
const fragment = dom.createDocumentFragment();
|
|
273
|
+
for (const node of nodes) {
|
|
274
|
+
if (dom.isNode(node)) {
|
|
275
|
+
dom.appendChild(fragment, node);
|
|
276
|
+
currentNodes.push(node);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const markerParent = dom.getParentNode(marker);
|
|
280
|
+
if (markerParent) {
|
|
281
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
effect(() => {
|
|
286
|
+
const show = typeof condition === 'function' ? condition() : condition;
|
|
287
|
+
|
|
288
|
+
if (show) {
|
|
289
|
+
// Enter
|
|
290
|
+
if (activeAnimation) {
|
|
291
|
+
activeAnimation.dispose();
|
|
292
|
+
activeAnimation = null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If we have leave content, animate it out first
|
|
296
|
+
if (currentNodes.length > 0 && _shouldAnimate()) {
|
|
297
|
+
isLeaving.set(true);
|
|
298
|
+
const nodesToRemove = [...currentNodes];
|
|
299
|
+
currentNodes = [];
|
|
300
|
+
|
|
301
|
+
// Animate leave on old nodes
|
|
302
|
+
const leaveAnims = nodesToRemove
|
|
303
|
+
.filter(n => typeof n.animate === 'function')
|
|
304
|
+
.map(n => animate(n, leave, { duration, easing }));
|
|
305
|
+
|
|
306
|
+
if (leaveAnims.length > 0) {
|
|
307
|
+
Promise.all(leaveAnims.map(a => a.finished)).then(() => {
|
|
308
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
309
|
+
isLeaving.set(false);
|
|
310
|
+
_enterContent();
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
} else {
|
|
314
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
315
|
+
isLeaving.set(false);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
_removeCurrentNodes();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_enterContent();
|
|
322
|
+
} else {
|
|
323
|
+
// Leave
|
|
324
|
+
if (activeAnimation) {
|
|
325
|
+
activeAnimation.dispose();
|
|
326
|
+
activeAnimation = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (currentNodes.length > 0) {
|
|
330
|
+
isLeaving.set(true);
|
|
331
|
+
const nodesToRemove = [...currentNodes];
|
|
332
|
+
currentNodes = [];
|
|
333
|
+
|
|
334
|
+
if (_shouldAnimate()) {
|
|
335
|
+
const leaveAnims = nodesToRemove
|
|
336
|
+
.filter(n => typeof n.animate === 'function')
|
|
337
|
+
.map(n => animate(n, leave, { duration, easing }));
|
|
338
|
+
|
|
339
|
+
if (leaveAnims.length > 0) {
|
|
340
|
+
Promise.all(leaveAnims.map(a => a.finished)).then(() => {
|
|
341
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
342
|
+
isLeaving.set(false);
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
349
|
+
isLeaving.set(false);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
function _enterContent() {
|
|
355
|
+
if (!onEnter) return;
|
|
356
|
+
|
|
357
|
+
const result = onEnter();
|
|
358
|
+
if (!result) return;
|
|
359
|
+
|
|
360
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
361
|
+
_insertNodes(nodes);
|
|
362
|
+
|
|
363
|
+
if (_shouldAnimate()) {
|
|
364
|
+
isEntering.set(true);
|
|
365
|
+
const enterAnims = currentNodes
|
|
366
|
+
.filter(n => typeof n.animate === 'function')
|
|
367
|
+
.map(n => animate(n, enter, { duration, easing }));
|
|
368
|
+
|
|
369
|
+
if (enterAnims.length > 0) {
|
|
370
|
+
Promise.all(enterAnims.map(a => a.finished)).then(() => {
|
|
371
|
+
isEntering.set(false);
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
isEntering.set(false);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
onCleanup(() => {
|
|
380
|
+
if (activeAnimation) activeAnimation.dispose();
|
|
381
|
+
_removeCurrentNodes();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
container,
|
|
386
|
+
isEntering,
|
|
387
|
+
isLeaving,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// =============================================================================
|
|
392
|
+
// useSpring()
|
|
393
|
+
// =============================================================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Spring-based animation using damped harmonic oscillator
|
|
397
|
+
*
|
|
398
|
+
* @param {number|Function} target - Target value (or reactive function)
|
|
399
|
+
* @param {Object} [options]
|
|
400
|
+
* @param {number} [options.stiffness=170] - Spring stiffness
|
|
401
|
+
* @param {number} [options.damping=26] - Damping coefficient
|
|
402
|
+
* @param {number} [options.mass=1] - Mass
|
|
403
|
+
* @param {number} [options.precision=0.01] - Settle precision
|
|
404
|
+
* @returns {Object} { value, isAnimating, set, dispose }
|
|
405
|
+
*/
|
|
406
|
+
export function useSpring(target, options = {}) {
|
|
407
|
+
const {
|
|
408
|
+
stiffness = 170,
|
|
409
|
+
damping = 26,
|
|
410
|
+
mass = 1,
|
|
411
|
+
precision = 0.01,
|
|
412
|
+
} = options;
|
|
413
|
+
|
|
414
|
+
const initial = typeof target === 'function' ? target() : target;
|
|
415
|
+
const value = pulse(initial);
|
|
416
|
+
const isAnimating = pulse(false);
|
|
417
|
+
|
|
418
|
+
let currentValue = initial;
|
|
419
|
+
let velocity = 0;
|
|
420
|
+
let targetValue = initial;
|
|
421
|
+
let rafId = null;
|
|
422
|
+
let lastTime = 0;
|
|
423
|
+
|
|
424
|
+
function _step(time) {
|
|
425
|
+
if (!lastTime) {
|
|
426
|
+
lastTime = time;
|
|
427
|
+
rafId = requestAnimationFrame(_step);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const dt = Math.min((time - lastTime) / 1000, 0.064); // Cap at ~16fps minimum
|
|
432
|
+
lastTime = time;
|
|
433
|
+
|
|
434
|
+
// Damped spring: F = -kx - cv
|
|
435
|
+
const displacement = currentValue - targetValue;
|
|
436
|
+
const springForce = -stiffness * displacement;
|
|
437
|
+
const dampingForce = -damping * velocity;
|
|
438
|
+
const acceleration = (springForce + dampingForce) / mass;
|
|
439
|
+
|
|
440
|
+
velocity += acceleration * dt;
|
|
441
|
+
currentValue += velocity * dt;
|
|
442
|
+
|
|
443
|
+
// Check if settled
|
|
444
|
+
if (Math.abs(velocity) < precision && Math.abs(displacement) < precision) {
|
|
445
|
+
currentValue = targetValue;
|
|
446
|
+
velocity = 0;
|
|
447
|
+
value.set(currentValue);
|
|
448
|
+
isAnimating.set(false);
|
|
449
|
+
rafId = null;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
value.set(currentValue);
|
|
454
|
+
rafId = requestAnimationFrame(_step);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function _startAnimation() {
|
|
458
|
+
if (!_shouldAnimate()) {
|
|
459
|
+
currentValue = targetValue;
|
|
460
|
+
value.set(currentValue);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
isAnimating.set(true);
|
|
465
|
+
lastTime = 0;
|
|
466
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
467
|
+
rafId = requestAnimationFrame(_step);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Watch target changes if reactive
|
|
471
|
+
if (typeof target === 'function') {
|
|
472
|
+
effect(() => {
|
|
473
|
+
const newTarget = target();
|
|
474
|
+
if (newTarget !== targetValue) {
|
|
475
|
+
targetValue = newTarget;
|
|
476
|
+
_startAnimation();
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function set(newTarget) {
|
|
482
|
+
targetValue = newTarget;
|
|
483
|
+
_startAnimation();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function dispose() {
|
|
487
|
+
if (rafId) {
|
|
488
|
+
cancelAnimationFrame(rafId);
|
|
489
|
+
rafId = null;
|
|
490
|
+
}
|
|
491
|
+
isAnimating.set(false);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
onCleanup(dispose);
|
|
495
|
+
|
|
496
|
+
return { value, isAnimating, set, dispose };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// stagger()
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Stagger animation across multiple elements
|
|
505
|
+
*
|
|
506
|
+
* @param {Array<HTMLElement>} elements - Elements to animate
|
|
507
|
+
* @param {Array<Object>|Object} keyframes - Keyframes
|
|
508
|
+
* @param {Object} [options]
|
|
509
|
+
* @param {number} [options.duration] - Duration per element
|
|
510
|
+
* @param {number} [options.staggerDelay=50] - Delay between each element
|
|
511
|
+
* @param {string} [options.easing] - Easing function
|
|
512
|
+
* @returns {Array<Object>} Array of AnimationControl objects
|
|
513
|
+
*/
|
|
514
|
+
export function stagger(elements, keyframes, options = {}) {
|
|
515
|
+
const { staggerDelay = 50, ...animOptions } = options;
|
|
516
|
+
|
|
517
|
+
return elements.map((element, index) => {
|
|
518
|
+
return animate(element, keyframes, {
|
|
519
|
+
...animOptions,
|
|
520
|
+
delay: (animOptions.delay ?? 0) + index * staggerDelay,
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// =============================================================================
|
|
526
|
+
// DEFAULT EXPORT
|
|
527
|
+
// =============================================================================
|
|
528
|
+
|
|
529
|
+
export default {
|
|
530
|
+
animate,
|
|
531
|
+
useTransition,
|
|
532
|
+
useSpring,
|
|
533
|
+
stagger,
|
|
534
|
+
configureAnimations,
|
|
535
|
+
};
|
package/runtime/dom-advanced.js
CHANGED
|
@@ -21,63 +21,142 @@ const log = loggers.dom;
|
|
|
21
21
|
*
|
|
22
22
|
* @param {*|Function} children - Children to render (static or reactive)
|
|
23
23
|
* @param {string|HTMLElement} target - Target selector or element
|
|
24
|
-
* @
|
|
24
|
+
* @param {Object} [options] - Portal options
|
|
25
|
+
* @param {string} [options.key] - Unique key for multiple portals to same target
|
|
26
|
+
* @param {boolean} [options.prepend=false] - Insert at beginning of target
|
|
27
|
+
* @param {Function} [options.onMount] - Called when children are mounted
|
|
28
|
+
* @param {Function} [options.onUnmount] - Called when children are unmounted
|
|
29
|
+
* @returns {Comment} Marker node with dispose(), moveTo(), getNodes() methods
|
|
25
30
|
*/
|
|
26
|
-
export function portal(children, target) {
|
|
31
|
+
export function portal(children, target, options = {}) {
|
|
27
32
|
const dom = getAdapter();
|
|
28
|
-
const {
|
|
33
|
+
const { key = null, prepend = false, onMount = null, onUnmount = null } = options;
|
|
34
|
+
|
|
35
|
+
let currentTarget = null;
|
|
36
|
+
let currentSelector = null;
|
|
37
|
+
let mountedNodes = [];
|
|
38
|
+
let disposed = false;
|
|
39
|
+
let disposeEffect = null;
|
|
40
|
+
|
|
41
|
+
// Resolve target
|
|
42
|
+
function _resolveTarget(tgt) {
|
|
43
|
+
const { element: resolvedTarget, selector } = resolveSelector(tgt, 'portal');
|
|
44
|
+
currentTarget = resolvedTarget;
|
|
45
|
+
currentSelector = selector;
|
|
46
|
+
return resolvedTarget;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolvedTarget = _resolveTarget(target);
|
|
29
50
|
|
|
30
51
|
if (!resolvedTarget) {
|
|
31
|
-
log.warn(`Portal target not found: "${
|
|
32
|
-
|
|
52
|
+
log.warn(`Portal target not found: "${currentSelector}"`);
|
|
53
|
+
const marker = dom.createComment(key ? `portal:${key}` : 'portal-target-not-found');
|
|
54
|
+
marker.dispose = () => {};
|
|
55
|
+
marker.moveTo = () => {};
|
|
56
|
+
marker.getNodes = () => [];
|
|
57
|
+
return marker;
|
|
33
58
|
}
|
|
34
59
|
|
|
35
|
-
const marker = dom.createComment('portal');
|
|
36
|
-
let mountedNodes = [];
|
|
60
|
+
const marker = dom.createComment(key ? `portal:${key}` : 'portal');
|
|
37
61
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
function _clearNodes() {
|
|
63
|
+
for (const node of mountedNodes) {
|
|
64
|
+
dom.removeNode(node);
|
|
65
|
+
if (node._pulseUnmount) {
|
|
66
|
+
for (const cb of node._pulseUnmount) cb();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (mountedNodes.length > 0) {
|
|
70
|
+
onUnmount?.();
|
|
71
|
+
}
|
|
72
|
+
mountedNodes = [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _mountNodes(nodes, tgt) {
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (dom.isNode(node)) {
|
|
78
|
+
if (prepend && tgt.firstChild) {
|
|
79
|
+
dom.insertBefore(tgt, node, tgt.firstChild);
|
|
80
|
+
} else {
|
|
81
|
+
dom.appendChild(tgt, node);
|
|
46
82
|
}
|
|
83
|
+
mountedNodes.push(node);
|
|
47
84
|
}
|
|
48
|
-
|
|
85
|
+
}
|
|
86
|
+
if (mountedNodes.length > 0) {
|
|
87
|
+
onMount?.();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
49
90
|
|
|
91
|
+
function _renderChildren(tgt) {
|
|
92
|
+
if (typeof children === 'function') {
|
|
50
93
|
const result = children();
|
|
51
94
|
if (result) {
|
|
52
95
|
const nodes = Array.isArray(result) ? result : [result];
|
|
53
|
-
|
|
54
|
-
if (dom.isNode(node)) {
|
|
55
|
-
dom.appendChild(resolvedTarget, node);
|
|
56
|
-
mountedNodes.push(node);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
96
|
+
_mountNodes(nodes, tgt);
|
|
59
97
|
}
|
|
98
|
+
} else {
|
|
99
|
+
const nodes = Array.isArray(children) ? children : [children];
|
|
100
|
+
_mountNodes(nodes, tgt);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle reactive children
|
|
105
|
+
if (typeof children === 'function') {
|
|
106
|
+
disposeEffect = effect(() => {
|
|
107
|
+
if (disposed) return;
|
|
108
|
+
_clearNodes();
|
|
109
|
+
_renderChildren(currentTarget);
|
|
60
110
|
});
|
|
61
111
|
} else {
|
|
62
|
-
|
|
63
|
-
const nodes = Array.isArray(children) ? children : [children];
|
|
64
|
-
for (const node of nodes) {
|
|
65
|
-
if (dom.isNode(node)) {
|
|
66
|
-
dom.appendChild(resolvedTarget, node);
|
|
67
|
-
mountedNodes.push(node);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
112
|
+
_renderChildren(currentTarget);
|
|
70
113
|
}
|
|
71
114
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Manually dispose portal (remove all portaled nodes)
|
|
117
|
+
*/
|
|
118
|
+
marker.dispose = function () {
|
|
119
|
+
if (disposed) return;
|
|
120
|
+
disposed = true;
|
|
121
|
+
_clearNodes();
|
|
122
|
+
if (disposeEffect) disposeEffect();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Move portaled content to a new target
|
|
127
|
+
* @param {string|HTMLElement} newTarget - New target selector or element
|
|
128
|
+
*/
|
|
129
|
+
marker.moveTo = function (newTarget) {
|
|
130
|
+
if (disposed) return;
|
|
131
|
+
|
|
132
|
+
const resolved = _resolveTarget(newTarget);
|
|
133
|
+
if (!resolved) {
|
|
134
|
+
log.warn(`Portal moveTo target not found: "${currentSelector}"`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Re-mount existing nodes in new target
|
|
139
|
+
const existingNodes = [...mountedNodes];
|
|
140
|
+
// Remove from old target without unmount callbacks
|
|
141
|
+
for (const node of existingNodes) {
|
|
75
142
|
dom.removeNode(node);
|
|
76
|
-
if (node._pulseUnmount) {
|
|
77
|
-
for (const cb of node._pulseUnmount) cb();
|
|
78
|
-
}
|
|
79
143
|
}
|
|
80
|
-
|
|
144
|
+
mountedNodes = [];
|
|
145
|
+
|
|
146
|
+
// Re-mount in new target
|
|
147
|
+
_mountNodes(existingNodes, resolved);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get currently portaled nodes
|
|
152
|
+
* @returns {Array} Currently mounted nodes
|
|
153
|
+
*/
|
|
154
|
+
marker.getNodes = function () {
|
|
155
|
+
return [...mountedNodes];
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Attach cleanup for parent effect cascade
|
|
159
|
+
marker._pulseUnmount = [() => marker.dispose()];
|
|
81
160
|
|
|
82
161
|
return marker;
|
|
83
162
|
}
|