canvasengine 2.0.0-beta.42 → 2.0.0-beta.43

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 (49) hide show
  1. package/dist/{DebugRenderer-BosXI2Pd.js → DebugRenderer-K2IZBznP.js} +2 -2
  2. package/dist/{DebugRenderer-BosXI2Pd.js.map → DebugRenderer-K2IZBznP.js.map} +1 -1
  3. package/dist/components/Button.d.ts +3 -0
  4. package/dist/components/Button.d.ts.map +1 -1
  5. package/dist/components/DOMElement.d.ts.map +1 -1
  6. package/dist/components/Graphic.d.ts +1 -1
  7. package/dist/components/Graphic.d.ts.map +1 -1
  8. package/dist/components/index.d.ts +1 -0
  9. package/dist/components/index.d.ts.map +1 -1
  10. package/dist/components/types/DisplayObject.d.ts +12 -16
  11. package/dist/components/types/DisplayObject.d.ts.map +1 -1
  12. package/dist/directives/FocusNavigation.d.ts +71 -0
  13. package/dist/directives/FocusNavigation.d.ts.map +1 -0
  14. package/dist/directives/KeyboardControls.d.ts.map +1 -1
  15. package/dist/directives/ViewportFollow.d.ts.map +1 -1
  16. package/dist/engine/FocusManager.d.ts +174 -0
  17. package/dist/engine/FocusManager.d.ts.map +1 -0
  18. package/dist/engine/reactive.d.ts.map +1 -1
  19. package/dist/hooks/useFocus.d.ts +61 -0
  20. package/dist/hooks/useFocus.d.ts.map +1 -0
  21. package/dist/{index-DNwqVzaq.js → index-B4hYyfVE.js} +5469 -4676
  22. package/dist/index-B4hYyfVE.js.map +1 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.global.js +7 -7
  26. package/dist/index.global.js.map +1 -1
  27. package/dist/index.js +66 -60
  28. package/package.json +2 -2
  29. package/src/components/Button.ts +7 -4
  30. package/src/components/Canvas.ts +1 -1
  31. package/src/components/DOMContainer.ts +27 -2
  32. package/src/components/DOMElement.ts +37 -29
  33. package/src/components/DisplayObject.ts +15 -3
  34. package/src/components/FocusContainer.ts +372 -0
  35. package/src/components/Graphic.ts +43 -48
  36. package/src/components/Sprite.ts +4 -2
  37. package/src/components/Viewport.ts +65 -26
  38. package/src/components/index.ts +2 -1
  39. package/src/components/types/DisplayObject.ts +7 -4
  40. package/src/directives/Controls.ts +1 -1
  41. package/src/directives/ControlsBase.ts +1 -1
  42. package/src/directives/FocusNavigation.ts +252 -0
  43. package/src/directives/KeyboardControls.ts +12 -8
  44. package/src/directives/ViewportFollow.ts +8 -5
  45. package/src/engine/FocusManager.ts +495 -0
  46. package/src/engine/reactive.ts +20 -19
  47. package/src/hooks/useFocus.ts +94 -0
  48. package/src/index.ts +2 -0
  49. package/dist/index-DNwqVzaq.js.map +0 -1
@@ -0,0 +1,495 @@
1
+ import { isSignal, signal, Signal } from "@signe/reactive";
2
+ import { Element, isElementFrozen } from "./reactive";
3
+ import { CanvasViewport } from "../components/Viewport";
4
+ import { SignalOrPrimitive } from "../components/types";
5
+
6
+ /**
7
+ * Options for scroll behavior when navigating to focused elements
8
+ *
9
+ * @property padding - Padding around the element in pixels (default: 0)
10
+ * @property smooth - Enable smooth scrolling animation (default: false)
11
+ * @property center - Center the element in the viewport (default: true)
12
+ * @property duration - Animation duration in ms if smooth=true (default: 300)
13
+ */
14
+ export interface ScrollOptions {
15
+ padding?: number;
16
+ smooth?: boolean;
17
+ center?: boolean;
18
+ duration?: number;
19
+ }
20
+
21
+ /**
22
+ * Data structure for a focus container
23
+ */
24
+ interface FocusContainerData {
25
+ id: string;
26
+ element?: Element;
27
+ focusables: Map<number, Element>;
28
+ currentIndex: Signal<number | null>;
29
+ focusedElement: Signal<Element | null>;
30
+ onFocusChange?: (index: number, element: Element | null) => void;
31
+ autoScroll?: boolean | ScrollOptions;
32
+ viewport?: CanvasViewport;
33
+ throttle?: number;
34
+ lastNavigateTime?: number;
35
+ tabindex?: SignalOrPrimitive<number>;
36
+ tabindexSubscription?: any;
37
+ }
38
+
39
+ /**
40
+ * Central manager for focus navigation system
41
+ *
42
+ * Manages focusable elements within containers, handles navigation,
43
+ * and provides scroll integration with Viewport.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const manager = FocusManager.getInstance();
48
+ * manager.registerContainer('menu', containerData);
49
+ * manager.navigate('menu', 'next');
50
+ * ```
51
+ */
52
+ export class FocusManager {
53
+ private static instance: FocusManager | null = null;
54
+ private containers: Map<string, FocusContainerData> = new Map();
55
+ private scrollAnimations: Map<string, { startTime: number; startX: number; startY: number; targetX: number; targetY: number; duration: number }> = new Map();
56
+
57
+ /**
58
+ * Get the singleton instance of FocusManager
59
+ *
60
+ * @returns The FocusManager instance
61
+ */
62
+ static getInstance(): FocusManager {
63
+ if (!FocusManager.instance) {
64
+ FocusManager.instance = new FocusManager();
65
+ }
66
+ return FocusManager.instance;
67
+ }
68
+
69
+ /**
70
+ * Register a focus container
71
+ *
72
+ * @param id - Unique identifier for the container
73
+ * @param data - Container data including signals and callbacks
74
+ */
75
+ registerContainer(id: string, data: Omit<FocusContainerData, 'id'>): void {
76
+ this.containers.set(id, { ...data, id });
77
+ }
78
+
79
+ /**
80
+ * Update a focus container's data
81
+ *
82
+ * @param id - Container identifier
83
+ * @param data - Partial container data to update
84
+ */
85
+ updateContainer(id: string, data: Partial<Omit<FocusContainerData, 'id'>>): void {
86
+ const container = this.containers.get(id);
87
+ if (container) {
88
+ this.containers.set(id, { ...container, ...data });
89
+ }
90
+ }
91
+
92
+ setTabindex(id: string, tabindex: SignalOrPrimitive<number>): void {
93
+ const container = this.containers.get(id);
94
+ if (!container) return;
95
+
96
+ // Cleanup previous subscription
97
+ if (container.tabindexSubscription) {
98
+ container.tabindexSubscription.unsubscribe();
99
+ }
100
+
101
+ container.tabindex = tabindex;
102
+
103
+ if (isSignal(tabindex)) {
104
+ container.tabindexSubscription = (tabindex as Signal<number>).observable.subscribe((value: any) => {
105
+ if (value !== null && value !== container.currentIndex()) {
106
+ this.setIndex(id, value);
107
+ }
108
+ });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Unregister a focus container
114
+ *
115
+ * @param id - Container identifier to remove
116
+ */
117
+ unregisterContainer(id: string): void {
118
+ this.containers.delete(id);
119
+ this.scrollAnimations.delete(id);
120
+ }
121
+
122
+ /**
123
+ * Register a focusable element in a container
124
+ *
125
+ * @param containerId - Container identifier
126
+ * @param element - Element to register
127
+ * @param index - Focus index for this element
128
+ */
129
+ registerFocusable(containerId: string, element: Element, index: number): void {
130
+ const container = this.containers.get(containerId);
131
+ if (!container) {
132
+ console.warn(`FocusContainer with id "${containerId}" not found`);
133
+ return;
134
+ }
135
+ container.focusables.set(index, element);
136
+
137
+ // If this is the index we are supposed to be at, set it now
138
+ const currentTabindex = isSignal(container.tabindex) ? (container.tabindex as Signal<number>)() : container.tabindex;
139
+ if (currentTabindex === index && container.currentIndex() === null) {
140
+ this.setIndex(containerId, index);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Unregister a focusable element from a container
146
+ *
147
+ * @param containerId - Container identifier
148
+ * @param index - Focus index to remove
149
+ */
150
+ unregisterFocusable(containerId: string, index: number): void {
151
+ const container = this.containers.get(containerId);
152
+ if (!container) return;
153
+ container.focusables.delete(index);
154
+ }
155
+
156
+ /**
157
+ * Navigate to next or previous focusable element
158
+ *
159
+ * @param containerId - Container identifier
160
+ * @param direction - Navigation direction ('next' or 'previous')
161
+ */
162
+ navigate(containerId: string, direction: 'next' | 'previous'): void {
163
+ const container = this.containers.get(containerId);
164
+ if (!container) {
165
+ return;
166
+ }
167
+
168
+ // Check if container is frozen (including parent containers)
169
+ if (container.element && isElementFrozen(container.element)) {
170
+ return;
171
+ }
172
+
173
+ // Handle throttling
174
+ if (container.throttle) {
175
+ const now = Date.now();
176
+ const lastTime = container.lastNavigateTime || 0;
177
+ if (now - lastTime < container.throttle) {
178
+ return;
179
+ }
180
+ container.lastNavigateTime = now;
181
+ }
182
+
183
+ const currentIndex = container.currentIndex();
184
+ const focusableIndices = Array.from(container.focusables.keys()).sort((a, b) => a - b);
185
+
186
+ if (focusableIndices.length === 0) return;
187
+
188
+ let newIndex: number | null = null;
189
+
190
+ if (currentIndex === null) {
191
+ // No current focus, go to first or last
192
+ newIndex = direction === 'next' ? focusableIndices[0] : focusableIndices[focusableIndices.length - 1];
193
+ } else {
194
+ const currentIndexPos = focusableIndices.indexOf(currentIndex);
195
+ if (direction === 'next') {
196
+ if (currentIndexPos < focusableIndices.length - 1) {
197
+ newIndex = focusableIndices[currentIndexPos + 1];
198
+ } else {
199
+ // Wrap around to first
200
+ newIndex = focusableIndices[0];
201
+ }
202
+ } else {
203
+ if (currentIndexPos > 0) {
204
+ newIndex = focusableIndices[currentIndexPos - 1];
205
+ } else {
206
+ // Wrap around to last
207
+ newIndex = focusableIndices[focusableIndices.length - 1];
208
+ }
209
+ }
210
+ }
211
+
212
+ if (newIndex !== null) {
213
+ const tabindex = container.tabindex;
214
+ if (isSignal(tabindex)) {
215
+ (tabindex as any).set(newIndex);
216
+ } else {
217
+ this.setIndex(containerId, newIndex);
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Set the focus index for a container
224
+ *
225
+ * @param containerId - Container identifier
226
+ * @param index - Focus index to set
227
+ */
228
+ setIndex(containerId: string, index: number): void {
229
+ const container = this.containers.get(containerId);
230
+ if (!container) return;
231
+
232
+ const element = container.focusables.get(index);
233
+ if (!element) {
234
+ console.warn(`No focusable element at index ${index} in container "${containerId}"`);
235
+ return;
236
+ }
237
+
238
+ container.currentIndex.set(index);
239
+ container.focusedElement.set(element);
240
+
241
+ // Sync back to tabindex signal if it exists
242
+ const tabindex = container.tabindex;
243
+ if (isSignal(tabindex) && (tabindex as any)() !== index) {
244
+ (tabindex as any).set(index);
245
+ }
246
+
247
+ // Trigger callback
248
+ if (container.onFocusChange) {
249
+ container.onFocusChange(index, element);
250
+ }
251
+ // Handle DOM focus and scrolling
252
+ const instance = element.componentInstance as any;
253
+ if (instance && instance.element && typeof instance.element.focus === 'function') {
254
+ const domElement = instance.element as HTMLElement;
255
+ // Focus the native DOM element so :focus styles apply
256
+ domElement.focus();
257
+
258
+ // Scroll the element into view, centering it in the scrollable parent
259
+ if (typeof domElement.scrollIntoView === 'function') {
260
+ domElement.scrollIntoView({
261
+ block: 'center',
262
+ behavior: 'smooth'
263
+ });
264
+ }
265
+ }
266
+
267
+ // Handle auto-scroll if enabled
268
+ if (container.autoScroll) {
269
+ const viewport = container.viewport;
270
+ if (viewport) {
271
+ const options: ScrollOptions = typeof container.autoScroll === 'boolean'
272
+ ? { center: true }
273
+ : container.autoScroll;
274
+ this.scrollToElement(containerId, index, viewport, options);
275
+ }
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Get the element at a specific index
281
+ *
282
+ * @param containerId - Container identifier
283
+ * @param index - Focus index
284
+ * @returns Element at index or null
285
+ */
286
+ getElement(containerId: string, index: number): Element | null {
287
+ const container = this.containers.get(containerId);
288
+ if (!container) return null;
289
+ return container.focusables.get(index) || null;
290
+ }
291
+
292
+ /**
293
+ * Get current focus index for a container
294
+ *
295
+ * @param containerId - Container identifier
296
+ * @returns Current index signal
297
+ */
298
+ getCurrentIndexSignal(containerId: string): Signal<number | null> | null {
299
+ const container = this.containers.get(containerId);
300
+ return container ? container.currentIndex : null;
301
+ }
302
+
303
+ /**
304
+ * Get current focused element signal for a container
305
+ *
306
+ * @param containerId - Container identifier
307
+ * @returns Current element signal
308
+ */
309
+ getFocusedElementSignal(containerId: string): Signal<Element | null> | null {
310
+ const container = this.containers.get(containerId);
311
+ return container ? container.focusedElement : null;
312
+ }
313
+
314
+ /**
315
+ * Check if an element is visible in the viewport
316
+ *
317
+ * @param element - Element to check
318
+ * @param viewport - Viewport to check against (optional)
319
+ * @returns True if element is visible
320
+ */
321
+ isElementVisible(element: Element, viewport?: CanvasViewport): boolean {
322
+ if (!viewport) return true;
323
+
324
+ const bounds = this.getElementBounds(element);
325
+ const visibleBounds = viewport.getVisibleBounds();
326
+
327
+ return (
328
+ bounds.x < visibleBounds.right &&
329
+ bounds.x + bounds.width > visibleBounds.left &&
330
+ bounds.y < visibleBounds.bottom &&
331
+ bounds.y + bounds.height > visibleBounds.top
332
+ );
333
+ }
334
+
335
+ /**
336
+ * Get global bounds of an element
337
+ *
338
+ * @param element - Element to get bounds for
339
+ * @returns Bounds object with x, y, width, height
340
+ */
341
+ getElementBounds(element: Element): { x: number; y: number; width: number; height: number } {
342
+ const instance = element.componentInstance;
343
+ if (!instance) {
344
+ return { x: 0, y: 0, width: 0, height: 0 };
345
+ }
346
+
347
+ // Get local bounds
348
+ const localBounds = instance.getLocalBounds();
349
+
350
+ // Get global position
351
+ const globalPos = instance.getGlobalPosition();
352
+
353
+ return {
354
+ x: globalPos.x,
355
+ y: globalPos.y,
356
+ width: localBounds.width,
357
+ height: localBounds.height
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Scroll viewport to show an element
363
+ *
364
+ * @param containerId - Container identifier
365
+ * @param index - Focus index of element to scroll to
366
+ * @param viewport - Viewport instance (optional, uses container's viewport if not provided)
367
+ * @param options - Scroll options
368
+ */
369
+ scrollToElement(
370
+ containerId: string,
371
+ index: number,
372
+ viewport?: CanvasViewport,
373
+ options: ScrollOptions = {}
374
+ ): void {
375
+ const container = this.containers.get(containerId);
376
+ if (!container) return;
377
+
378
+ const element = container.focusables.get(index);
379
+ if (!element) return;
380
+
381
+ const targetViewport = viewport || container.viewport;
382
+ if (!targetViewport) return;
383
+
384
+ const bounds = this.getElementBounds(element);
385
+ const visibleBounds = targetViewport.getVisibleBounds();
386
+ const padding = options.padding || 0;
387
+ const center = options.center !== false; // Default to true
388
+ const smooth = options.smooth || false;
389
+ const duration = options.duration || 300;
390
+
391
+ // Check if element is already visible
392
+ if (this.isElementVisible(element, targetViewport)) {
393
+ // Element is visible, but check if we need to center it
394
+ if (center) {
395
+ const centerX = bounds.x + bounds.width / 2;
396
+ const centerY = bounds.y + bounds.height / 2;
397
+
398
+ if (smooth) {
399
+ this.animateScroll(containerId, targetViewport, centerX, centerY, duration);
400
+ } else {
401
+ targetViewport.moveCenter(centerX, centerY);
402
+ }
403
+ }
404
+ return;
405
+ }
406
+
407
+ // Element is not visible, scroll to it
408
+ if (center) {
409
+ const centerX = bounds.x + bounds.width / 2;
410
+ const centerY = bounds.y + bounds.height / 2;
411
+
412
+ if (smooth) {
413
+ this.animateScroll(containerId, targetViewport, centerX, centerY, duration);
414
+ } else {
415
+ targetViewport.moveCenter(centerX, centerY);
416
+ }
417
+ } else {
418
+ // Scroll to make element visible with padding
419
+ const targetX = bounds.x - padding;
420
+ const targetY = bounds.y - padding;
421
+ const targetWidth = bounds.width + padding * 2;
422
+ const targetHeight = bounds.height + padding * 2;
423
+
424
+ if (smooth) {
425
+ // For smooth fit, we'll animate to center
426
+ const centerX = bounds.x + bounds.width / 2;
427
+ const centerY = bounds.y + bounds.height / 2;
428
+ this.animateScroll(containerId, targetViewport, centerX, centerY, duration);
429
+ } else {
430
+ targetViewport.fit(targetX, targetY, targetWidth, targetHeight, padding);
431
+ }
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Animate smooth scrolling
437
+ *
438
+ * @param containerId - Container identifier
439
+ * @param viewport - Viewport instance
440
+ * @param targetX - Target X position
441
+ * @param targetY - Target Y position
442
+ * @param duration - Animation duration in ms
443
+ */
444
+ private animateScroll(
445
+ containerId: string,
446
+ viewport: CanvasViewport,
447
+ targetX: number,
448
+ targetY: number,
449
+ duration: number
450
+ ): void {
451
+ const currentCenter = viewport.center;
452
+ const startX = currentCenter.x;
453
+ const startY = currentCenter.y;
454
+
455
+ const animation = {
456
+ startTime: Date.now(),
457
+ startX,
458
+ startY,
459
+ targetX,
460
+ targetY,
461
+ duration
462
+ };
463
+
464
+ this.scrollAnimations.set(containerId, animation);
465
+
466
+ // Use requestAnimationFrame for smooth animation
467
+ const animate = () => {
468
+ const anim = this.scrollAnimations.get(containerId);
469
+ if (!anim) return;
470
+
471
+ const elapsed = Date.now() - anim.startTime;
472
+ const progress = Math.min(elapsed / anim.duration, 1);
473
+
474
+ // Easing function (ease-out)
475
+ const eased = 1 - Math.pow(1 - progress, 3);
476
+
477
+ const currentX = anim.startX + (anim.targetX - anim.startX) * eased;
478
+ const currentY = anim.startY + (anim.targetY - anim.startY) * eased;
479
+
480
+ viewport.moveCenter(currentX, currentY);
481
+
482
+ if (progress < 1) {
483
+ requestAnimationFrame(animate);
484
+ } else {
485
+ this.scrollAnimations.delete(containerId);
486
+ }
487
+ };
488
+
489
+ requestAnimationFrame(animate);
490
+ }
491
+ }
492
+
493
+ // Export singleton instance
494
+ export const focusManager = FocusManager.getInstance();
495
+
@@ -9,6 +9,7 @@ import {
9
9
  map,
10
10
  of,
11
11
  share,
12
+ shareReplay,
12
13
  switchMap,
13
14
  debounceTime,
14
15
  distinctUntilChanged,
@@ -110,7 +111,7 @@ export function registerAllComponents() {
110
111
  if (componentsRegistered) {
111
112
  return;
112
113
  }
113
-
114
+
114
115
  // Components are registered when their modules are imported
115
116
  // Since bootstrap.ts imports all components, they should already be registered
116
117
  // when bootstrapCanvas() is called. This function just marks that registration
@@ -129,10 +130,10 @@ export function registerAllComponents() {
129
130
  */
130
131
  export function isElementFrozen(element: Element): boolean {
131
132
  if (!element) return false;
132
-
133
+
133
134
  // Check if this element itself is frozen
134
135
  const freezeProp = element.propObservables?.freeze ?? element.props?.freeze;
135
-
136
+
136
137
  if (freezeProp !== undefined && freezeProp !== null) {
137
138
  // Handle Signal<boolean>
138
139
  if (isSignal(freezeProp)) {
@@ -144,12 +145,12 @@ export function isElementFrozen(element: Element): boolean {
144
145
  return true;
145
146
  }
146
147
  }
147
-
148
+
148
149
  // Check if any parent is frozen (recursive check)
149
150
  if (element.parent) {
150
151
  return isElementFrozen(element.parent);
151
152
  }
152
-
153
+
153
154
  return false;
154
155
  }
155
156
 
@@ -161,7 +162,7 @@ export function isElementFrozen(element: Element): boolean {
161
162
  */
162
163
  function handleAnimatedSignalsFreeze(element: Element, shouldPause: boolean) {
163
164
  if (!element.propObservables) return;
164
-
165
+
165
166
  const processValue = (value: any) => {
166
167
  if (isSignal(value) && isAnimatedSignal(value as any)) {
167
168
  const animatedSig = value as unknown as AnimatedSignal<any>;
@@ -175,7 +176,7 @@ function handleAnimatedSignalsFreeze(element: Element, shouldPause: boolean) {
175
176
  Object.values(value).forEach(processValue);
176
177
  }
177
178
  };
178
-
179
+
179
180
  Object.values(element.propObservables).forEach(processValue);
180
181
  }
181
182
 
@@ -272,19 +273,19 @@ export function createComponent(tag: string, props?: Props): Element {
272
273
  }
273
274
  return;
274
275
  }
275
-
276
+
276
277
  // Handle freeze prop as signal
277
278
  if (key === "freeze") {
278
279
  element.isFrozen = _value() === true;
279
-
280
+
280
281
  // Pause/resume animatedSignals based on initial freeze state
281
282
  handleAnimatedSignalsFreeze(element, element.isFrozen);
282
-
283
+
283
284
  element.propSubscriptions.push(
284
285
  _value.observable.subscribe((freezeValue) => {
285
286
  const wasFrozen = element.isFrozen;
286
287
  element.isFrozen = freezeValue === true;
287
-
288
+
288
289
  // Handle animatedSignal pause/resume when freeze state changes
289
290
  if (wasFrozen !== element.isFrozen) {
290
291
  handleAnimatedSignalsFreeze(element, element.isFrozen);
@@ -293,7 +294,7 @@ export function createComponent(tag: string, props?: Props): Element {
293
294
  );
294
295
  return;
295
296
  }
296
-
297
+
297
298
  element.propSubscriptions.push(
298
299
  _value.observable.subscribe((value) => {
299
300
  // Block updates if element is frozen
@@ -304,12 +305,12 @@ export function createComponent(tag: string, props?: Props): Element {
304
305
  }
305
306
  return;
306
307
  }
307
-
308
+
308
309
  // Resume animatedSignal if it was paused
309
310
  if (isAnimatedSignal(_value as any)) {
310
311
  (_value as unknown as AnimatedSignal<any>).resume();
311
312
  }
312
-
313
+
313
314
  _set(path, key, value);
314
315
  if (element.directives[key]) {
315
316
  element.directives[key].onUpdate?.(value, element);
@@ -324,8 +325,8 @@ export function createComponent(tag: string, props?: Props): Element {
324
325
  instance.onUpdate?.(
325
326
  path == ""
326
327
  ? {
327
- [key]: value,
328
- }
328
+ [key]: value,
329
+ }
329
330
  : set({}, path + "." + key, value)
330
331
  );
331
332
  })
@@ -334,7 +335,7 @@ export function createComponent(tag: string, props?: Props): Element {
334
335
  // Handle freeze prop as direct boolean
335
336
  if (key === "freeze") {
336
337
  element.isFrozen = value === true;
337
-
338
+
338
339
  // Pause/resume animatedSignals based on freeze state
339
340
  handleAnimatedSignalsFreeze(element, element.isFrozen);
340
341
  }
@@ -457,7 +458,7 @@ export function createComponent(tag: string, props?: Props): Element {
457
458
 
458
459
  element.props.context = actualParent.props.context;
459
460
  element.parent = actualParent;
460
-
461
+
461
462
  // Inherit freeze state from parent if element doesn't have its own freeze prop
462
463
  if (!element.propObservables?.freeze && !element.props?.freeze && isElementFrozen(actualParent)) {
463
464
  element.isFrozen = true;
@@ -847,7 +848,7 @@ export function loop<T>(
847
848
  elements.forEach(el => destroyElement(el));
848
849
  };
849
850
  });
850
- });
851
+ }).pipe(shareReplay({ bufferSize: 1, refCount: true }));
851
852
  }
852
853
 
853
854
  /**