canvasengine 2.0.0-beta.41 → 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 (53) hide show
  1. package/dist/{DebugRenderer-BxfW34YG.js → DebugRenderer-K2IZBznP.js} +2 -2
  2. package/dist/{DebugRenderer-BxfW34YG.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/bootstrap.d.ts +33 -1
  19. package/dist/engine/bootstrap.d.ts.map +1 -1
  20. package/dist/engine/reactive.d.ts +20 -0
  21. package/dist/engine/reactive.d.ts.map +1 -1
  22. package/dist/hooks/useFocus.d.ts +61 -0
  23. package/dist/hooks/useFocus.d.ts.map +1 -0
  24. package/dist/{index-BnuKipxl.js → index-B4hYyfVE.js} +5479 -4677
  25. package/dist/index-B4hYyfVE.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.global.js +7 -7
  29. package/dist/index.global.js.map +1 -1
  30. package/dist/index.js +70 -63
  31. package/package.json +2 -2
  32. package/src/components/Button.ts +7 -4
  33. package/src/components/Canvas.ts +1 -1
  34. package/src/components/DOMContainer.ts +27 -2
  35. package/src/components/DOMElement.ts +37 -29
  36. package/src/components/DisplayObject.ts +15 -3
  37. package/src/components/FocusContainer.ts +372 -0
  38. package/src/components/Graphic.ts +43 -48
  39. package/src/components/Sprite.ts +4 -2
  40. package/src/components/Viewport.ts +65 -26
  41. package/src/components/index.ts +2 -1
  42. package/src/components/types/DisplayObject.ts +7 -4
  43. package/src/directives/Controls.ts +1 -1
  44. package/src/directives/ControlsBase.ts +1 -1
  45. package/src/directives/FocusNavigation.ts +252 -0
  46. package/src/directives/KeyboardControls.ts +12 -8
  47. package/src/directives/ViewportFollow.ts +8 -5
  48. package/src/engine/FocusManager.ts +495 -0
  49. package/src/engine/bootstrap.ts +69 -2
  50. package/src/engine/reactive.ts +54 -18
  51. package/src/hooks/useFocus.ts +94 -0
  52. package/src/index.ts +3 -0
  53. package/dist/index-BnuKipxl.js.map +0 -1
@@ -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,
@@ -84,6 +85,41 @@ export function registerComponent(name, component) {
84
85
  components[name] = component;
85
86
  }
86
87
 
88
+ // Track if components have been registered to avoid duplicate imports
89
+ let componentsRegistered = false;
90
+
91
+ /**
92
+ * Registers all default CanvasEngine components.
93
+ *
94
+ * This function imports and registers all core components that are available by default.
95
+ * It's called automatically by bootstrapCanvas() if no custom component configuration is provided.
96
+ *
97
+ * Components register themselves when their modules are imported, so this function ensures
98
+ * all component modules are loaded. Since components call registerComponent() at module load time,
99
+ * importing them will automatically register them synchronously.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Register all default components manually
104
+ * registerAllComponents();
105
+ *
106
+ * // Now you can use any component
107
+ * const sprite = createComponent('Sprite', { image: 'hero.png' });
108
+ * ```
109
+ */
110
+ export function registerAllComponents() {
111
+ if (componentsRegistered) {
112
+ return;
113
+ }
114
+
115
+ // Components are registered when their modules are imported
116
+ // Since bootstrap.ts imports all components, they should already be registered
117
+ // when bootstrapCanvas() is called. This function just marks that registration
118
+ // has been attempted. If components aren't registered yet, they will be when
119
+ // bootstrap.ts imports them (which happens before bootstrapCanvas() is called).
120
+ componentsRegistered = true;
121
+ }
122
+
87
123
  /**
88
124
  * Checks if an element is currently frozen.
89
125
  * An element is frozen when the `freeze` prop is set to `true` (either as a boolean or Signal<boolean>),
@@ -94,10 +130,10 @@ export function registerComponent(name, component) {
94
130
  */
95
131
  export function isElementFrozen(element: Element): boolean {
96
132
  if (!element) return false;
97
-
133
+
98
134
  // Check if this element itself is frozen
99
135
  const freezeProp = element.propObservables?.freeze ?? element.props?.freeze;
100
-
136
+
101
137
  if (freezeProp !== undefined && freezeProp !== null) {
102
138
  // Handle Signal<boolean>
103
139
  if (isSignal(freezeProp)) {
@@ -109,12 +145,12 @@ export function isElementFrozen(element: Element): boolean {
109
145
  return true;
110
146
  }
111
147
  }
112
-
148
+
113
149
  // Check if any parent is frozen (recursive check)
114
150
  if (element.parent) {
115
151
  return isElementFrozen(element.parent);
116
152
  }
117
-
153
+
118
154
  return false;
119
155
  }
120
156
 
@@ -126,7 +162,7 @@ export function isElementFrozen(element: Element): boolean {
126
162
  */
127
163
  function handleAnimatedSignalsFreeze(element: Element, shouldPause: boolean) {
128
164
  if (!element.propObservables) return;
129
-
165
+
130
166
  const processValue = (value: any) => {
131
167
  if (isSignal(value) && isAnimatedSignal(value as any)) {
132
168
  const animatedSig = value as unknown as AnimatedSignal<any>;
@@ -140,7 +176,7 @@ function handleAnimatedSignalsFreeze(element: Element, shouldPause: boolean) {
140
176
  Object.values(value).forEach(processValue);
141
177
  }
142
178
  };
143
-
179
+
144
180
  Object.values(element.propObservables).forEach(processValue);
145
181
  }
146
182
 
@@ -237,19 +273,19 @@ export function createComponent(tag: string, props?: Props): Element {
237
273
  }
238
274
  return;
239
275
  }
240
-
276
+
241
277
  // Handle freeze prop as signal
242
278
  if (key === "freeze") {
243
279
  element.isFrozen = _value() === true;
244
-
280
+
245
281
  // Pause/resume animatedSignals based on initial freeze state
246
282
  handleAnimatedSignalsFreeze(element, element.isFrozen);
247
-
283
+
248
284
  element.propSubscriptions.push(
249
285
  _value.observable.subscribe((freezeValue) => {
250
286
  const wasFrozen = element.isFrozen;
251
287
  element.isFrozen = freezeValue === true;
252
-
288
+
253
289
  // Handle animatedSignal pause/resume when freeze state changes
254
290
  if (wasFrozen !== element.isFrozen) {
255
291
  handleAnimatedSignalsFreeze(element, element.isFrozen);
@@ -258,7 +294,7 @@ export function createComponent(tag: string, props?: Props): Element {
258
294
  );
259
295
  return;
260
296
  }
261
-
297
+
262
298
  element.propSubscriptions.push(
263
299
  _value.observable.subscribe((value) => {
264
300
  // Block updates if element is frozen
@@ -269,12 +305,12 @@ export function createComponent(tag: string, props?: Props): Element {
269
305
  }
270
306
  return;
271
307
  }
272
-
308
+
273
309
  // Resume animatedSignal if it was paused
274
310
  if (isAnimatedSignal(_value as any)) {
275
311
  (_value as unknown as AnimatedSignal<any>).resume();
276
312
  }
277
-
313
+
278
314
  _set(path, key, value);
279
315
  if (element.directives[key]) {
280
316
  element.directives[key].onUpdate?.(value, element);
@@ -289,8 +325,8 @@ export function createComponent(tag: string, props?: Props): Element {
289
325
  instance.onUpdate?.(
290
326
  path == ""
291
327
  ? {
292
- [key]: value,
293
- }
328
+ [key]: value,
329
+ }
294
330
  : set({}, path + "." + key, value)
295
331
  );
296
332
  })
@@ -299,7 +335,7 @@ export function createComponent(tag: string, props?: Props): Element {
299
335
  // Handle freeze prop as direct boolean
300
336
  if (key === "freeze") {
301
337
  element.isFrozen = value === true;
302
-
338
+
303
339
  // Pause/resume animatedSignals based on freeze state
304
340
  handleAnimatedSignalsFreeze(element, element.isFrozen);
305
341
  }
@@ -422,7 +458,7 @@ export function createComponent(tag: string, props?: Props): Element {
422
458
 
423
459
  element.props.context = actualParent.props.context;
424
460
  element.parent = actualParent;
425
-
461
+
426
462
  // Inherit freeze state from parent if element doesn't have its own freeze prop
427
463
  if (!element.propObservables?.freeze && !element.props?.freeze && isElementFrozen(actualParent)) {
428
464
  element.isFrozen = true;
@@ -812,7 +848,7 @@ export function loop<T>(
812
848
  elements.forEach(el => destroyElement(el));
813
849
  };
814
850
  });
815
- });
851
+ }).pipe(shareReplay({ bufferSize: 1, refCount: true }));
816
852
  }
817
853
 
818
854
  /**
@@ -0,0 +1,94 @@
1
+ import { Signal } from "@signe/reactive";
2
+ import { Element } from "../engine/reactive";
3
+ import { focusManager } from "../engine/FocusManager";
4
+ import { effect } from "@signe/reactive";
5
+
6
+ /**
7
+ * Get the current focus index signal for a container
8
+ *
9
+ * Returns a reactive signal that updates when the focus index changes.
10
+ *
11
+ * @param containerId - Container identifier
12
+ * @returns Signal for current focus index, or null if container not found
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const focusIndex = useFocusIndex('myContainer');
17
+ * effect(() => {
18
+ * console.log('Current focus index:', focusIndex?.());
19
+ * });
20
+ * ```
21
+ */
22
+ export function useFocusIndex(containerId: string): Signal<number | null> | null {
23
+ return focusManager.getCurrentIndexSignal(containerId);
24
+ }
25
+
26
+ /**
27
+ * Get the current focused element signal for a container
28
+ *
29
+ * Returns a reactive signal that updates when the focused element changes.
30
+ *
31
+ * @param containerId - Container identifier
32
+ * @returns Signal for current focused element, or null if container not found
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const focusedElement = useFocusedElement('myContainer');
37
+ * effect(() => {
38
+ * const element = focusedElement?.();
39
+ * if (element) {
40
+ * console.log('Focused element:', element);
41
+ * }
42
+ * });
43
+ * ```
44
+ */
45
+ export function useFocusedElement(containerId: string): Signal<Element | null> | null {
46
+ return focusManager.getFocusedElementSignal(containerId);
47
+ }
48
+
49
+ /**
50
+ * Hook to react to focus changes
51
+ *
52
+ * Sets up a reactive effect that calls the callback whenever the focus changes.
53
+ *
54
+ * @param containerId - Container identifier
55
+ * @param callback - Function to call when focus changes
56
+ * @returns Cleanup function to unsubscribe
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * useFocusChange('myContainer', (index, element) => {
61
+ * console.log('Focus changed to index', index);
62
+ * if (element) {
63
+ * console.log('Focused element:', element);
64
+ * }
65
+ * });
66
+ * ```
67
+ */
68
+ export function useFocusChange(
69
+ containerId: string,
70
+ callback: (index: number | null, element: Element | null) => void
71
+ ): () => void {
72
+ const indexSignal = focusManager.getCurrentIndexSignal(containerId);
73
+ const elementSignal = focusManager.getFocusedElementSignal(containerId);
74
+
75
+ if (!indexSignal || !elementSignal) {
76
+ console.warn(`FocusContainer with id "${containerId}" not found`);
77
+ return () => {};
78
+ }
79
+
80
+ // Set up reactive effect
81
+ const subscription = effect(() => {
82
+ const index = indexSignal();
83
+ const element = elementSignal();
84
+ callback(index, element);
85
+ });
86
+
87
+ // Return cleanup function
88
+ return () => {
89
+ if (subscription && typeof subscription.unsubscribe === 'function') {
90
+ subscription.unsubscribe();
91
+ }
92
+ };
93
+ }
94
+
package/src/index.ts CHANGED
@@ -4,11 +4,14 @@ export * from '@signe/reactive'
4
4
  export { Howler } from 'howler'
5
5
  export * from './components'
6
6
  export * from './engine/reactive'
7
+ export { registerAllComponents } from './engine/reactive'
7
8
  export * from './engine/signal'
8
9
  export * from './engine/trigger'
9
10
  export * from './engine/bootstrap'
10
11
  export * from './engine/animation'
12
+ export { FocusManager, focusManager, type ScrollOptions } from './engine/FocusManager'
11
13
  export { useProps, useDefineProps } from './hooks/useProps'
14
+ export { useFocusIndex, useFocusedElement, useFocusChange } from './hooks/useFocus'
12
15
  export * from './utils/Ease'
13
16
  export * from './utils/RadialGradient'
14
17
  export * from './components/DisplayObject'