canvasengine 2.0.0-beta.44 → 2.0.0-beta.46

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 (83) hide show
  1. package/dist/components/Container.d.ts +86 -0
  2. package/dist/components/Container.d.ts.map +1 -0
  3. package/dist/components/DOMContainer.d.ts +98 -0
  4. package/dist/components/DOMContainer.d.ts.map +1 -0
  5. package/dist/components/DOMElement.d.ts +16 -5
  6. package/dist/components/DOMElement.d.ts.map +1 -1
  7. package/dist/components/DOMSprite.d.ts +108 -0
  8. package/dist/components/DOMSprite.d.ts.map +1 -0
  9. package/dist/components/DisplayObject.d.ts +94 -0
  10. package/dist/components/DisplayObject.d.ts.map +1 -0
  11. package/dist/components/FocusContainer.d.ts +129 -0
  12. package/dist/components/FocusContainer.d.ts.map +1 -0
  13. package/dist/components/Mesh.d.ts +208 -0
  14. package/dist/components/Mesh.d.ts.map +1 -0
  15. package/dist/components/Sprite.d.ts +242 -0
  16. package/dist/components/Sprite.d.ts.map +1 -0
  17. package/dist/components/Viewport.d.ts +121 -0
  18. package/dist/components/Viewport.d.ts.map +1 -0
  19. package/dist/components/index.d.ts +2 -1
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/directives/Controls.d.ts +4 -4
  22. package/dist/directives/Controls.d.ts.map +1 -1
  23. package/dist/directives/ControlsBase.d.ts +1 -0
  24. package/dist/directives/ControlsBase.d.ts.map +1 -1
  25. package/dist/directives/FocusNavigation.d.ts +4 -22
  26. package/dist/directives/FocusNavigation.d.ts.map +1 -1
  27. package/dist/directives/KeyboardControls.d.ts +1 -0
  28. package/dist/directives/KeyboardControls.d.ts.map +1 -1
  29. package/dist/directives/Scheduler.d.ts.map +1 -1
  30. package/dist/directives/Shake.d.ts +1 -0
  31. package/dist/directives/Shake.d.ts.map +1 -1
  32. package/dist/engine/FocusManager.d.ts +10 -9
  33. package/dist/engine/FocusManager.d.ts.map +1 -1
  34. package/dist/engine/bootstrap.d.ts +1 -0
  35. package/dist/engine/bootstrap.d.ts.map +1 -1
  36. package/dist/engine/directive.d.ts +1 -1
  37. package/dist/engine/directive.d.ts.map +1 -1
  38. package/dist/engine/reactive.d.ts +10 -0
  39. package/dist/engine/reactive.d.ts.map +1 -1
  40. package/dist/engine/signal.d.ts +19 -19
  41. package/dist/engine/signal.d.ts.map +1 -1
  42. package/dist/hooks/useFocus.d.ts.map +1 -1
  43. package/dist/index-DaGekQUW.js +2218 -0
  44. package/dist/index-DaGekQUW.js.map +1 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.global.js +3 -3
  48. package/dist/index.global.js.map +1 -1
  49. package/dist/index.js +11868 -86
  50. package/dist/index.js.map +1 -1
  51. package/dist/utils/tabindex.d.ts +16 -0
  52. package/dist/utils/tabindex.d.ts.map +1 -0
  53. package/package.json +1 -1
  54. package/src/components/DOMContainer.ts +186 -1
  55. package/src/components/DOMElement.ts +164 -37
  56. package/src/components/DOMSprite.ts +759 -0
  57. package/src/components/DisplayObject.ts +33 -7
  58. package/src/components/FocusContainer.ts +22 -26
  59. package/src/components/Sprite.ts +12 -3
  60. package/src/components/Text.ts +1 -1
  61. package/src/components/Viewport.ts +5 -5
  62. package/src/components/index.ts +2 -1
  63. package/src/directives/Controls.ts +5 -5
  64. package/src/directives/ControlsBase.ts +1 -0
  65. package/src/directives/FocusNavigation.ts +8 -146
  66. package/src/directives/KeyboardControls.ts +11 -2
  67. package/src/directives/Scheduler.ts +12 -4
  68. package/src/directives/Shake.ts +9 -6
  69. package/src/engine/FocusManager.ts +44 -29
  70. package/src/engine/bootstrap.ts +5 -2
  71. package/src/engine/directive.ts +2 -2
  72. package/src/engine/reactive.ts +137 -27
  73. package/src/engine/signal.ts +80 -23
  74. package/src/hooks/useFocus.ts +2 -5
  75. package/src/index.ts +2 -1
  76. package/src/types/pixi-cull.d.ts +7 -0
  77. package/src/utils/tabindex.ts +70 -0
  78. package/testing/index.ts +31 -3
  79. package/tsconfig.json +3 -2
  80. package/dist/DebugRenderer-P5sZ-0Tq.js +0 -172
  81. package/dist/DebugRenderer-P5sZ-0Tq.js.map +0 -1
  82. package/dist/index-VPoz4ufu.js +0 -13252
  83. package/dist/index-VPoz4ufu.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { Container, Point } from 'pixi.js';
2
2
  import { Directive, registerDirective } from '../engine/directive';
3
3
  import { Element } from '../engine/reactive';
4
- import { effect } from '@signe/reactive';
4
+ import { effect, isSignal } from '@signe/reactive';
5
5
  import { on, isTrigger, Trigger } from '../engine/trigger';
6
6
  import { useProps } from '../hooks/useProps';
7
7
  import { SignalOrPrimitive } from '../components/types';
@@ -128,6 +128,10 @@ export class Shake extends Directive {
128
128
  });
129
129
  }
130
130
 
131
+ private resolveSignalValue<T>(value: SignalOrPrimitive<T>): T {
132
+ return (isSignal(value as any) ? (value as any)() : value) as T;
133
+ }
134
+
131
135
  /**
132
136
  * Performs the shake animation using animatedSignal
133
137
  * @param data - Optional data passed from the trigger that can override default options
@@ -139,10 +143,10 @@ export class Shake extends Directive {
139
143
  const shakeProps = this.shakeProps;
140
144
 
141
145
  // Use data from trigger to override defaults if provided
142
- const intensity = data?.intensity ?? shakeProps.intensity();
143
- const duration = data?.duration ?? shakeProps.duration();
144
- const frequency = data?.frequency ?? shakeProps.frequency();
145
- const direction = data?.direction ?? shakeProps.direction();
146
+ const intensity = data?.intensity ?? this.resolveSignalValue(shakeProps.intensity);
147
+ const duration = data?.duration ?? this.resolveSignalValue(shakeProps.duration);
148
+ const frequency = data?.frequency ?? this.resolveSignalValue(shakeProps.frequency);
149
+ const direction = data?.direction ?? this.resolveSignalValue(shakeProps.direction);
146
150
 
147
151
  // Stop any existing animation and clean up
148
152
  if (this.positionEffect) {
@@ -292,4 +296,3 @@ export class Shake extends Directive {
292
296
  }
293
297
 
294
298
  registerDirective('shake', Shake);
295
-
@@ -1,4 +1,4 @@
1
- import { isSignal, signal, Signal } from "@signe/reactive";
1
+ import { isSignal, signal, Signal, WritableSignal, WritableObjectSignal } from "@signe/reactive";
2
2
  import { Element, isElementFrozen } from "./reactive";
3
3
  import { CanvasViewport } from "../components/Viewport";
4
4
  import { SignalOrPrimitive } from "../components/types";
@@ -21,19 +21,20 @@ export interface ScrollOptions {
21
21
  /**
22
22
  * Data structure for a focus container
23
23
  */
24
+ type WritableElementSignal = WritableSignal<Element | null> | WritableObjectSignal<Element | null>;
25
+
24
26
  interface FocusContainerData {
25
27
  id: string;
26
- element?: Element;
27
- focusables: Map<number, Element>;
28
- currentIndex: Signal<number | null>;
29
- focusedElement: Signal<Element | null>;
28
+ element?: Element<any>;
29
+ focusables: Map<number, Element<any>>;
30
+ currentIndex: WritableSignal<number | null>;
31
+ focusedElement: WritableElementSignal;
30
32
  onFocusChange?: (index: number, element: Element | null) => void;
31
33
  autoScroll?: boolean | ScrollOptions;
32
34
  viewport?: CanvasViewport;
33
- throttle?: number;
34
- lastNavigateTime?: number;
35
- tabindex?: SignalOrPrimitive<number>;
35
+ tabindex?: SignalOrPrimitive<number> | null;
36
36
  tabindexSubscription?: any;
37
+ pendingIndex?: number;
37
38
  }
38
39
 
39
40
  /**
@@ -89,7 +90,7 @@ export class FocusManager {
89
90
  }
90
91
  }
91
92
 
92
- setTabindex(id: string, tabindex: SignalOrPrimitive<number>): void {
93
+ setTabindex(id: string, tabindex?: SignalOrPrimitive<number> | null): void {
93
94
  const container = this.containers.get(id);
94
95
  if (!container) return;
95
96
 
@@ -98,10 +99,20 @@ export class FocusManager {
98
99
  container.tabindexSubscription.unsubscribe();
99
100
  }
100
101
 
102
+ if (tabindex === undefined || tabindex === null) {
103
+ container.tabindex = undefined;
104
+ return;
105
+ }
106
+
101
107
  container.tabindex = tabindex;
102
108
 
109
+ const currentTabindex = isSignal(tabindex) ? (tabindex as Signal<number>)() : tabindex;
110
+ if (typeof currentTabindex === "number") {
111
+ this.setIndex(id, currentTabindex);
112
+ }
113
+
103
114
  if (isSignal(tabindex)) {
104
- container.tabindexSubscription = (tabindex as Signal<number>).observable.subscribe((value: any) => {
115
+ container.tabindexSubscription = ((tabindex as Signal<number>).observable as any).subscribe((value: any) => {
105
116
  if (value !== null && value !== container.currentIndex()) {
106
117
  this.setIndex(id, value);
107
118
  }
@@ -136,8 +147,9 @@ export class FocusManager {
136
147
 
137
148
  // If this is the index we are supposed to be at, set it now
138
149
  const currentTabindex = isSignal(container.tabindex) ? (container.tabindex as Signal<number>)() : container.tabindex;
139
- if (currentTabindex === index && container.currentIndex() === null) {
140
- this.setIndex(containerId, index);
150
+ if (container.pendingIndex === index || (currentTabindex === index && container.currentIndex() === null)) {
151
+ container.pendingIndex = undefined;
152
+ this.applyFocus(container, containerId, index, element);
141
153
  }
142
154
  }
143
155
 
@@ -170,16 +182,6 @@ export class FocusManager {
170
182
  return;
171
183
  }
172
184
 
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
185
  const currentIndex = container.currentIndex();
184
186
  const focusableIndices = Array.from(container.focusables.keys()).sort((a, b) => a - b);
185
187
 
@@ -211,7 +213,7 @@ export class FocusManager {
211
213
 
212
214
  if (newIndex !== null) {
213
215
  const tabindex = container.tabindex;
214
- if (isSignal(tabindex)) {
216
+ if (isSignal(tabindex) && typeof (tabindex as any).set === "function") {
215
217
  (tabindex as any).set(newIndex);
216
218
  } else {
217
219
  this.setIndex(containerId, newIndex);
@@ -231,16 +233,24 @@ export class FocusManager {
231
233
 
232
234
  const element = container.focusables.get(index);
233
235
  if (!element) {
234
- console.warn(`No focusable element at index ${index} in container "${containerId}"`);
236
+ container.pendingIndex = index;
235
237
  return;
236
238
  }
239
+ this.applyFocus(container, containerId, index, element);
240
+ }
237
241
 
242
+ private applyFocus(
243
+ container: FocusContainerData,
244
+ containerId: string,
245
+ index: number,
246
+ element: Element
247
+ ): void {
238
248
  container.currentIndex.set(index);
239
249
  container.focusedElement.set(element);
240
250
 
241
251
  // Sync back to tabindex signal if it exists
242
252
  const tabindex = container.tabindex;
243
- if (isSignal(tabindex) && (tabindex as any)() !== index) {
253
+ if (isSignal(tabindex) && (tabindex as any)() !== index && typeof (tabindex as any).set === "function") {
244
254
  (tabindex as any).set(index);
245
255
  }
246
256
 
@@ -308,7 +318,7 @@ export class FocusManager {
308
318
  */
309
319
  getFocusedElementSignal(containerId: string): Signal<Element | null> | null {
310
320
  const container = this.containers.get(containerId);
311
- return container ? container.focusedElement : null;
321
+ return container ? (container.focusedElement as unknown as Signal<Element | null>) : null;
312
322
  }
313
323
 
314
324
  /**
@@ -345,10 +355,16 @@ export class FocusManager {
345
355
  }
346
356
 
347
357
  // Get local bounds
348
- const localBounds = instance.getLocalBounds();
358
+ const localBounds = instance.getLocalBounds?.();
359
+ if (!localBounds) {
360
+ return { x: 0, y: 0, width: 0, height: 0 };
361
+ }
349
362
 
350
363
  // Get global position
351
- const globalPos = instance.getGlobalPosition();
364
+ const globalPos = instance.getGlobalPosition?.();
365
+ if (!globalPos) {
366
+ return { x: 0, y: 0, width: 0, height: 0 };
367
+ }
352
368
 
353
369
  return {
354
370
  x: globalPos.x,
@@ -492,4 +508,3 @@ export class FocusManager {
492
508
 
493
509
  // Export singleton instance
494
510
  export const focusManager = FocusManager.getInstance();
495
-
@@ -1,4 +1,3 @@
1
- import '@pixi/layout';
2
1
  import { Application, ApplicationOptions } from "pixi.js";
3
2
  import { ComponentFunction, h } from "./signal";
4
3
  import { useProps } from '../hooks/useProps';
@@ -31,6 +30,7 @@ export interface BootstrapOptions extends ApplicationOptions {
31
30
  [name: string]: any; // ComponentClass
32
31
  };
33
32
  autoRegister?: boolean; // true by default if components is not provided
33
+ enableLayout?: boolean; // true by default
34
34
  }
35
35
 
36
36
  /**
@@ -63,7 +63,10 @@ export interface BootstrapOptions extends ApplicationOptions {
63
63
  */
64
64
  export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>, options?: BootstrapOptions) => {
65
65
  // Extract component registration options
66
- const { components, autoRegister, ...appOptions } = options ?? {};
66
+ const { components, autoRegister, enableLayout, ...appOptions } = options ?? {};
67
+ if (enableLayout !== false) {
68
+ await import('@pixi/layout');
69
+ }
67
70
 
68
71
  // Handle component registration
69
72
  if (components) {
@@ -13,11 +13,11 @@ export function registerDirective(name: string, directive: any) {
13
13
  directives[name] = directive
14
14
  }
15
15
 
16
- export function applyDirective(element: Element, directiveName: string) {
16
+ export function applyDirective(element: Element<any>, directiveName: string) {
17
17
  if (!directives[directiveName]) {
18
18
  return null
19
19
  }
20
20
  const directive = new directives[directiveName]()
21
21
  directive.onInit?.(element)
22
22
  return directive
23
- }
23
+ }
@@ -81,6 +81,72 @@ export const isPrimitive = (value) => {
81
81
  );
82
82
  };
83
83
 
84
+ const DOM_ROUTING_MAP: Record<string, string> = {
85
+ Sprite: "DOMSprite",
86
+ };
87
+
88
+ const DOM_ALLOWED_TAGS = new Set(["DOMContainer", "DOMElement", "DOMSprite"]);
89
+ const DOM_UNSUPPORTED_TAGS = new Set([
90
+ "Canvas",
91
+ "Container",
92
+ "Graphics",
93
+ "Rect",
94
+ "Circle",
95
+ "Ellipse",
96
+ "Triangle",
97
+ "Svg",
98
+ "Mesh",
99
+ "Scene",
100
+ "ParticlesEmitter",
101
+ "Sprite",
102
+ "Video",
103
+ "Text",
104
+ "TilingSprite",
105
+ "Viewport",
106
+ "NineSliceSprite",
107
+ "Button",
108
+ "Joystick",
109
+ "FocusContainer",
110
+ ]);
111
+
112
+ const hasDomAncestor = (element: Element | null): boolean => {
113
+ let current = element;
114
+ while (current) {
115
+ if (current.tag === "DOMContainer" || current.tag === "DOMElement") {
116
+ return true;
117
+ }
118
+ current = current.parent;
119
+ }
120
+ return false;
121
+ };
122
+
123
+ const cleanupElementForRouting = (element: Element) => {
124
+ element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
125
+ element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
126
+ element.effectUnmounts?.forEach((fn) => fn?.());
127
+ };
128
+
129
+ const routeDomComponent = (parent: Element, child: Element): Element => {
130
+ if (!hasDomAncestor(parent)) {
131
+ return child;
132
+ }
133
+ if (DOM_ALLOWED_TAGS.has(child.tag)) {
134
+ return child;
135
+ }
136
+ const routedTag = DOM_ROUTING_MAP[child.tag];
137
+ if (routedTag) {
138
+ cleanupElementForRouting(child);
139
+ const routedProps = child.propObservables ?? child.props;
140
+ return createComponent(routedTag, routedProps);
141
+ }
142
+ if (DOM_UNSUPPORTED_TAGS.has(child.tag)) {
143
+ throw new Error(
144
+ `Component ${child.tag} is not implemented for DOMContainer context yet. Only Sprite is supported.`
145
+ );
146
+ }
147
+ return child;
148
+ };
149
+
84
150
  export function registerComponent(name, component) {
85
151
  components[name] = component;
86
152
  }
@@ -120,6 +186,54 @@ export function registerAllComponents() {
120
186
  componentsRegistered = true;
121
187
  }
122
188
 
189
+ /**
190
+ * Checks if all dependencies are ready (not undefined).
191
+ * Handles signals synchronously and promises asynchronously.
192
+ * For reactive signals, sets up subscriptions to mount when all become ready.
193
+ *
194
+ * @param deps - Array of signals, promises, or direct values
195
+ * @returns Promise<boolean> - true if all dependencies are ready
196
+ */
197
+ export async function checkDependencies(
198
+ deps: any[]
199
+ ): Promise<boolean> {
200
+ const values = await Promise.all(
201
+ deps.map(async (dep) => {
202
+ if (isSignal(dep)) {
203
+ return dep(); // Read current signal value
204
+ } else if (isPromise(dep)) {
205
+ return await dep; // Await promise resolution
206
+ }
207
+ return dep; // Direct value
208
+ })
209
+ );
210
+ return values.every((v) => v !== undefined);
211
+ }
212
+
213
+ export function waitForDependencies(deps: any[]): Promise<void> {
214
+ return new Promise(async (resolve) => {
215
+ const ready = await checkDependencies(deps);
216
+ if (ready) {
217
+ resolve();
218
+ return;
219
+ }
220
+
221
+ const signalDeps = deps.filter((dep) => isSignal(dep));
222
+ if (signalDeps.length === 0) {
223
+ return;
224
+ }
225
+
226
+ const signalObservables = signalDeps.map((sig) => sig.observable);
227
+ const subscription = combineLatest(signalObservables).subscribe(async () => {
228
+ const allReady = await checkDependencies(deps);
229
+ if (allReady) {
230
+ subscription.unsubscribe();
231
+ resolve();
232
+ }
233
+ });
234
+ });
235
+ }
236
+
123
237
  /**
124
238
  * Checks if an element is currently frozen.
125
239
  * An element is frozen when the `freeze` prop is set to `true` (either as a boolean or Signal<boolean>),
@@ -376,22 +490,12 @@ export function createComponent(tag: string, props?: Props): Element {
376
490
  * @param deps - Array of signals, promises, or direct values
377
491
  * @returns Promise<boolean> - true if all dependencies are ready
378
492
  */
379
- async function checkDependencies(
380
- deps: any[]
381
- ): Promise<boolean> {
382
- const values = await Promise.all(
383
- deps.map(async (dep) => {
384
- if (isSignal(dep)) {
385
- return dep(); // Read current signal value
386
- } else if (isPromise(dep)) {
387
- return await dep; // Await promise resolution
388
- }
389
- return dep; // Direct value
390
- })
391
- );
392
- return values.every((v) => v !== undefined);
393
- }
394
493
 
494
+
495
+ /**
496
+ * Sets up subscriptions to reactive signal dependencies.
497
+ * When all signals become defined, mounts the component.
498
+ */
395
499
  /**
396
500
  * Sets up subscriptions to reactive signal dependencies.
397
501
  * When all signals become defined, mounts the component.
@@ -571,8 +675,9 @@ export function createComponent(tag: string, props?: Props): Element {
571
675
  // Handle observable component recursively
572
676
  await createElement(parent, c);
573
677
  } else if (isElement(c)) {
574
- onMount(parent, c, index + 1);
575
- propagateContext(c);
678
+ const routed = routeDomComponent(parent, c);
679
+ onMount(parent, routed, index + 1);
680
+ propagateContext(routed);
576
681
  }
577
682
  });
578
683
  return;
@@ -583,8 +688,9 @@ export function createComponent(tag: string, props?: Props): Element {
583
688
  // Handle observable component recursively
584
689
  await createElement(parent, component);
585
690
  } else if (isElement(component)) {
586
- onMount(parent, component);
587
- propagateContext(component);
691
+ const routed = routeDomComponent(parent, component);
692
+ onMount(parent, routed);
693
+ propagateContext(routed);
588
694
  }
589
695
  } else {
590
696
  component.forEach(async (comp) => {
@@ -592,16 +698,18 @@ export function createComponent(tag: string, props?: Props): Element {
592
698
  // Handle observable component recursively
593
699
  await createElement(parent, comp);
594
700
  } else if (isElement(comp)) {
595
- onMount(parent, comp);
596
- propagateContext(comp);
701
+ const routed = routeDomComponent(parent, comp);
702
+ onMount(parent, routed);
703
+ propagateContext(routed);
597
704
  }
598
705
  });
599
706
  }
600
707
  });
601
708
  } else if (isElement(value)) {
602
709
  // Handle direct Element emission
603
- onMount(parent, value);
604
- propagateContext(value);
710
+ const routed = routeDomComponent(parent, value);
711
+ onMount(parent, routed);
712
+ propagateContext(routed);
605
713
  } else if (Array.isArray(value)) {
606
714
  // Handle array of elements (which can also be observables)
607
715
  value.forEach(async (element) => {
@@ -609,8 +717,9 @@ export function createComponent(tag: string, props?: Props): Element {
609
717
  // Handle observable element recursively
610
718
  await createElement(parent, element);
611
719
  } else if (isElement(element)) {
612
- onMount(parent, element);
613
- propagateContext(element);
720
+ const routed = routeDomComponent(parent, element);
721
+ onMount(parent, routed);
722
+ propagateContext(routed);
614
723
  }
615
724
  });
616
725
  }
@@ -621,8 +730,9 @@ export function createComponent(tag: string, props?: Props): Element {
621
730
  // Store subscription for cleanup
622
731
  parent.effectSubscriptions.push(subscription);
623
732
  } else if (isElement(child)) {
624
- onMount(parent, child);
625
- await propagateContext(child);
733
+ const routed = routeDomComponent(parent, child);
734
+ onMount(parent, routed);
735
+ await propagateContext(routed);
626
736
  }
627
737
  }
628
738
 
@@ -2,8 +2,10 @@ import {
2
2
  Observable,
3
3
  Subscription
4
4
  } from "rxjs";
5
+ import { isSignal } from "@signe/reactive";
5
6
  import type { Element } from "./reactive";
6
- import { isElementFrozen } from "./reactive";
7
+ import { isElementFrozen, waitForDependencies } from "./reactive";
8
+ import { isPromise } from "./utils";
7
9
  import { Tick } from "../directives/Scheduler";
8
10
  import { Container } from "../components";
9
11
 
@@ -22,16 +24,16 @@ export let mountTracker: MountFunction | null = null;
22
24
  * @param {(element: Element) => void} fn - The function to be called on mount.
23
25
  * @example
24
26
  * ```ts
25
- * mount((el) => {
26
- * console.log('mounted', el);
27
+ * mount((el) => {
28
+ * console.log('mounted', el);
27
29
  * });
28
30
  * ```
29
31
  * Unmount the component by returning a function:
30
32
  * ```ts
31
- * mount((el) => {
32
- * console.log('mounted', el);
33
+ * mount((el) => {
34
+ * console.log('mounted', el);
33
35
  * return () => {
34
- * console.log('unmounted', el);
36
+ * console.log('unmounted', el);
35
37
  * }
36
38
  * });
37
39
  * ```
@@ -45,8 +47,8 @@ export function mount(fn: (element: Element) => void) {
45
47
  * @param {(tickValue: Tick, element: Element) => void} fn - The function to be called on each tick.
46
48
  * @example
47
49
  * ```ts
48
- * tick((tickValue, el) => {
49
- * console.log('tick', tickValue, el);
50
+ * tick((tickValue, el) => {
51
+ * console.log('tick', tickValue, el);
50
52
  * });
51
53
  * ```
52
54
  */
@@ -78,29 +80,29 @@ export function tick(fn: (tickValue: Tick, element: Element) => void) {
78
80
  * @returns {ReturnType<C>}
79
81
  * @example
80
82
  * ```ts
81
- * const el = h(MyComponent, {
82
- * x: 100,
83
- * y: 100,
84
- * });
83
+ * const el = h(MyComponent, {
84
+ * x: 100,
85
+ * y: 100,
86
+ * });
85
87
  * ```
86
88
  *
87
89
  * with children:
88
90
  * ```ts
89
- * const el = h(MyComponent, {
90
- * x: 100,
91
- * y: 100,
92
- * },
93
- * h(MyChildComponent, {
94
- * x: 50,
95
- * y: 50,
96
- * }),
91
+ * const el = h(MyComponent, {
92
+ * x: 100,
93
+ * y: 100,
94
+ * },
95
+ * h(MyChildComponent, {
96
+ * x: 50,
97
+ * y: 50,
98
+ * }),
97
99
  * );
98
100
  * ```
99
101
  */
100
- export function h<C extends ComponentFunction<any>>(
102
+ function _h<C extends ComponentFunction<any>>(
101
103
  componentFunction: C | Element,
102
104
  props: Parameters<C>[0] = {} as Parameters<C>[0],
103
- ...children: any[]
105
+ children: any[]
104
106
  ): ReturnType<C> {
105
107
  const allSubscriptions = new Set<Subscription>();
106
108
  const allMounts = new Set<MountFunction>();
@@ -124,7 +126,7 @@ export function h<C extends ComponentFunction<any>>(
124
126
  component = componentFunction[0]
125
127
  }
126
128
  else {
127
- component = h(Container, {}, ...componentFunction) as Element
129
+ component = _h(Container, {}, componentFunction) as Element
128
130
  }
129
131
  }
130
132
  else if ('tag' in componentFunction) {
@@ -167,3 +169,58 @@ export function h<C extends ComponentFunction<any>>(
167
169
 
168
170
  return component as ReturnType<C>;
169
171
  }
172
+
173
+ /**
174
+ * Add tracking for subscriptions and mounts, then create an element from a component function.
175
+ * @template C
176
+ * @param {C} componentFunction - The component function to create an element from.
177
+ * @param {Parameters<C>[0]} [props={}] - The props to pass to the component function.
178
+ * @param {...any[]} children - The children elements of the component.
179
+ * @returns {ReturnType<C>}
180
+ * @example
181
+ * ```ts
182
+ * const el = h(MyComponent, {
183
+ * x: 100,
184
+ * y: 100,
185
+ * });
186
+ * ```
187
+ *
188
+ * with children:
189
+ * ```ts
190
+ * const el = h(MyComponent, {
191
+ * x: 100,
192
+ * y: 100,
193
+ * },
194
+ * h(MyChildComponent, {
195
+ * x: 50,
196
+ * y: 50,
197
+ * }),
198
+ * );
199
+ * ```
200
+ */
201
+ export function h<C extends ComponentFunction<any>>(
202
+ componentFunction: C | Element,
203
+ props: Parameters<C>[0] = {} as Parameters<C>[0],
204
+ ...children: any[]
205
+ ): ReturnType<C> {
206
+ if (props?.dependencies) {
207
+ const hasPromise = props.dependencies.some(isPromise);
208
+ if (!hasPromise) {
209
+ const allReady = props.dependencies.every(dep => {
210
+ if (isSignal(dep)) return dep() !== undefined;
211
+ return dep !== undefined;
212
+ });
213
+ if (allReady) {
214
+ return _h(componentFunction, props, children);
215
+ }
216
+ }
217
+
218
+ return new Observable(subscriber => {
219
+ waitForDependencies(props.dependencies).then(() => {
220
+ const el = _h(componentFunction, props, children);
221
+ subscriber.next(el);
222
+ });
223
+ }) as any;
224
+ }
225
+ return _h(componentFunction, props, children);
226
+ }
@@ -78,7 +78,7 @@ export function useFocusChange(
78
78
  }
79
79
 
80
80
  // Set up reactive effect
81
- const subscription = effect(() => {
81
+ const effectResult = effect(() => {
82
82
  const index = indexSignal();
83
83
  const element = elementSignal();
84
84
  callback(index, element);
@@ -86,9 +86,6 @@ export function useFocusChange(
86
86
 
87
87
  // Return cleanup function
88
88
  return () => {
89
- if (subscription && typeof subscription.unsubscribe === 'function') {
90
- subscription.unsubscribe();
91
- }
89
+ effectResult.subscription?.unsubscribe();
92
90
  };
93
91
  }
94
-
package/src/index.ts CHANGED
@@ -14,7 +14,8 @@ export { useProps, useDefineProps } from './hooks/useProps'
14
14
  export { useFocusIndex, useFocusedElement, useFocusChange } from './hooks/useFocus'
15
15
  export * from './utils/Ease'
16
16
  export * from './utils/RadialGradient'
17
+ export * from './utils/tabindex'
17
18
  export * from './components/DisplayObject'
18
19
  export { isObservable } from 'rxjs'
19
20
  export * as Utils from './engine/utils'
20
- export * as Howl from 'howler'
21
+ export * as Howl from 'howler'
@@ -0,0 +1,7 @@
1
+ declare module "pixi-cull" {
2
+ export class Simple {
3
+ constructor(options?: any);
4
+ lists: Array<any>;
5
+ cull(bounds: any): void;
6
+ }
7
+ }