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.
- package/dist/{DebugRenderer-BxfW34YG.js → DebugRenderer-K2IZBznP.js} +2 -2
- package/dist/{DebugRenderer-BxfW34YG.js.map → DebugRenderer-K2IZBznP.js.map} +1 -1
- package/dist/components/Button.d.ts +3 -0
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/DOMElement.d.ts.map +1 -1
- package/dist/components/Graphic.d.ts +1 -1
- package/dist/components/Graphic.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/types/DisplayObject.d.ts +12 -16
- package/dist/components/types/DisplayObject.d.ts.map +1 -1
- package/dist/directives/FocusNavigation.d.ts +71 -0
- package/dist/directives/FocusNavigation.d.ts.map +1 -0
- package/dist/directives/KeyboardControls.d.ts.map +1 -1
- package/dist/directives/ViewportFollow.d.ts.map +1 -1
- package/dist/engine/FocusManager.d.ts +174 -0
- package/dist/engine/FocusManager.d.ts.map +1 -0
- package/dist/engine/bootstrap.d.ts +33 -1
- package/dist/engine/bootstrap.d.ts.map +1 -1
- package/dist/engine/reactive.d.ts +20 -0
- package/dist/engine/reactive.d.ts.map +1 -1
- package/dist/hooks/useFocus.d.ts +61 -0
- package/dist/hooks/useFocus.d.ts.map +1 -0
- package/dist/{index-BnuKipxl.js → index-B4hYyfVE.js} +5479 -4677
- package/dist/index-B4hYyfVE.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.js +7 -7
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -63
- package/package.json +2 -2
- package/src/components/Button.ts +7 -4
- package/src/components/Canvas.ts +1 -1
- package/src/components/DOMContainer.ts +27 -2
- package/src/components/DOMElement.ts +37 -29
- package/src/components/DisplayObject.ts +15 -3
- package/src/components/FocusContainer.ts +372 -0
- package/src/components/Graphic.ts +43 -48
- package/src/components/Sprite.ts +4 -2
- package/src/components/Viewport.ts +65 -26
- package/src/components/index.ts +2 -1
- package/src/components/types/DisplayObject.ts +7 -4
- package/src/directives/Controls.ts +1 -1
- package/src/directives/ControlsBase.ts +1 -1
- package/src/directives/FocusNavigation.ts +252 -0
- package/src/directives/KeyboardControls.ts +12 -8
- package/src/directives/ViewportFollow.ts +8 -5
- package/src/engine/FocusManager.ts +495 -0
- package/src/engine/bootstrap.ts +69 -2
- package/src/engine/reactive.ts +54 -18
- package/src/hooks/useFocus.ts +94 -0
- package/src/index.ts +3 -0
- package/dist/index-BnuKipxl.js.map +0 -1
package/src/engine/reactive.ts
CHANGED
|
@@ -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
|
-
|
|
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'
|