@appstrata/react 0.1.0

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Deep equality comparison for plain objects, arrays, and primitives.
3
+ *
4
+ * Used by ContextStore to determine which parts of AppContext
5
+ * actually changed, enabling granular re-renders.
6
+ *
7
+ * @internal
8
+ */
9
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
10
+ //# sourceMappingURL=deep-equal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-equal.d.ts","sourceRoot":"","sources":["../src/deep-equal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAkBzD"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Deep equality comparison for plain objects, arrays, and primitives.
3
+ *
4
+ * Used by ContextStore to determine which parts of AppContext
5
+ * actually changed, enabling granular re-renders.
6
+ *
7
+ * @internal
8
+ */
9
+ export function deepEqual(a, b) {
10
+ if (a === b)
11
+ return true;
12
+ if (typeof a !== typeof b)
13
+ return false;
14
+ if (typeof a !== "object" || a === null || b === null)
15
+ return false;
16
+ if (Array.isArray(a) !== Array.isArray(b))
17
+ return false;
18
+ const keysA = Object.keys(a);
19
+ const keysB = Object.keys(b);
20
+ if (keysA.length !== keysB.length)
21
+ return false;
22
+ for (const key of keysA) {
23
+ if (!deepEqual(a[key], b[key])) {
24
+ return false;
25
+ }
26
+ }
27
+ return true;
28
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Core React hooks for AppStrata.
3
+ *
4
+ * All hooks must be used within an AppStrataProvider.
5
+ */
6
+ import { LifecyclePhase, type AppContext, type Capability, type SignagePlayer, type Resource, type DeviceInfo, type Source } from "@appstrata/core";
7
+ /**
8
+ * Access the complete AppContext.
9
+ * Re-renders on ANY context change.
10
+ *
11
+ * Returns null while waiting for the player READY message.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * const context = useAppContext();
16
+ * if (!context) return <div>Loading...</div>;
17
+ * return <h1>{context.config.title as string}</h1>;
18
+ * ```
19
+ */
20
+ export declare function useAppContext(): AppContext | null;
21
+ /**
22
+ * Access just the config portion of AppContext.
23
+ * Only re-renders when config actually changes (deep comparison).
24
+ *
25
+ * @typeParam T - The expected config shape
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * const config = useConfig<{ title: string; bgcolor: string }>();
30
+ * if (!config) return null;
31
+ * return <h1 style={{ background: config.bgcolor }}>{config.title}</h1>;
32
+ * ```
33
+ */
34
+ export declare function useConfig<T extends Record<string, unknown> = Record<string, unknown>>(): T | null;
35
+ /**
36
+ * Access just the resources from AppContext.
37
+ * Only re-renders when resources actually change (deep comparison).
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const resources = useResources();
42
+ * const fonts = resources?.filter(r => r.category === "font");
43
+ * ```
44
+ */
45
+ export declare function useResources(): Resource[] | null;
46
+ /**
47
+ * Result of useResource hook.
48
+ */
49
+ export interface UseResourceResult {
50
+ /** Resolved URI (including data URIs for inline sources). Null if not found or not loaded. */
51
+ uri: string | null;
52
+ /** Raw Source object. Null if not found or not loaded. */
53
+ source: Source | null;
54
+ /** Full Resource object for advanced access. Null if not found or not loaded. */
55
+ resource: Resource | null;
56
+ }
57
+ /**
58
+ * Look up a single resource by category and name, returning a ready-to-use URI.
59
+ * Only re-renders when resources actually change (deep comparison).
60
+ *
61
+ * Optionally resolves a named variant instead of the main source.
62
+ *
63
+ * @param category - Resource category (e.g. "font", "image")
64
+ * @param name - Resource name (e.g. "heading", "logo")
65
+ * @param variant - Optional variant name (e.g. "thumbnail")
66
+ * @returns Object with `uri`, `source`, and `resource` fields
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * const { uri: fontUri } = useResource("font", "heading");
71
+ * const { uri: logoUri } = useResource("image", "logo");
72
+ *
73
+ * // With variant:
74
+ * const { uri: thumbUri } = useResource("image", "logo", "thumbnail");
75
+ * ```
76
+ */
77
+ export declare function useResource(category: string, name: string, variant?: string): UseResourceResult;
78
+ /**
79
+ * Load all font resources from the player context.
80
+ *
81
+ * Injects `<link>` stylesheet elements for every resource with
82
+ * `category === "font"`. Re-loads when the resource list changes
83
+ * (e.g. during CMS live preview).
84
+ *
85
+ * @returns `true` once all font stylesheets have loaded, `false` while loading
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * const fontsLoaded = useFonts();
90
+ * if (!fontsLoaded) return <div>Loading fonts...</div>;
91
+ * ```
92
+ */
93
+ export declare function useFonts(): boolean;
94
+ /**
95
+ * Load all stylesheet resources from the player context.
96
+ *
97
+ * Injects `<link>` stylesheet elements for every resource with
98
+ * `category === "stylesheet"`. Re-loads when the resource list changes
99
+ * (e.g. during CMS live preview).
100
+ *
101
+ * @returns `true` once all stylesheets have loaded, `false` while loading
102
+ *
103
+ * @example
104
+ * ```tsx
105
+ * const stylesLoaded = useStylesheets();
106
+ * if (!stylesLoaded) return <div>Loading styles...</div>;
107
+ * ```
108
+ */
109
+ export declare function useStylesheets(): boolean;
110
+ /**
111
+ * Access just the device information from AppContext.
112
+ * Only re-renders when device info actually changes (deep comparison).
113
+ *
114
+ * @example
115
+ * ```tsx
116
+ * const device = useDevice();
117
+ * if (!device) return null;
118
+ * return <footer>Playing on: {device.name}</footer>;
119
+ * ```
120
+ */
121
+ export declare function useDevice(): DeviceInfo | null;
122
+ /**
123
+ * Access the SignagePlayer instance directly.
124
+ * Returns a stable reference (singleton) -- never triggers re-renders.
125
+ *
126
+ * Use this for imperative operations or capabilities not covered by
127
+ * dedicated hooks.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * const player = usePlayer();
132
+ * const handleComplete = () => player.notifyComplete();
133
+ * ```
134
+ */
135
+ export declare function usePlayer(): SignagePlayer;
136
+ /**
137
+ * Check if a capability is available.
138
+ *
139
+ * Returns a tri-state value:
140
+ * - `null` while the player hasn't initialized yet (pre-READY)
141
+ * - `true` when the capability is supported
142
+ * - `false` when the capability is NOT supported
143
+ *
144
+ * Re-renders on any context change (delegates to useAppContext internally).
145
+ *
146
+ * @param capability - The capability to check (e.g., "storage", "proxy")
147
+ *
148
+ * @example
149
+ * ```tsx
150
+ * const hasStorage = useCapability("storage");
151
+ * if (hasStorage === null) return <div>Initializing...</div>;
152
+ * if (hasStorage) { ... }
153
+ * ```
154
+ */
155
+ export declare function useCapability(capability: Capability): boolean | null;
156
+ /**
157
+ * Lifecycle callbacks interface.
158
+ */
159
+ export interface LifecycleCallbacks {
160
+ /** Called when app is about to become visible */
161
+ onShow?: () => void | (() => void);
162
+ /** Called when app is now visible and should start playback */
163
+ onStart?: () => void | (() => void);
164
+ /** Called when app is about to be hidden */
165
+ onHide?: () => void | (() => void);
166
+ /** Called when app is now hidden and should stop playback */
167
+ onStop?: () => void | (() => void);
168
+ }
169
+ /**
170
+ * Subscribe to lifecycle events.
171
+ * Callbacks can optionally return cleanup functions.
172
+ *
173
+ * Auto-subscribes on mount, auto-cleans up on unmount.
174
+ *
175
+ * **Note:** Because `useEffect` runs after paint, handlers for transient phases
176
+ * like `Show` and `Hide` may not fire if the phase has already passed by the
177
+ * time the effect registers them. Use `onStart`/`onStop` for work that must
178
+ * not be skipped, or read `useLifecyclePhase()` to derive state from the
179
+ * current phase.
180
+ *
181
+ * @example
182
+ * ```tsx
183
+ * useLifecycle({
184
+ * onStart: () => {
185
+ * const interval = setInterval(tick, 1000);
186
+ * return () => clearInterval(interval); // cleanup
187
+ * },
188
+ * onStop: () => console.log("Stopped"),
189
+ * });
190
+ * ```
191
+ */
192
+ export declare function useLifecycle(callbacks: LifecycleCallbacks): void;
193
+ /**
194
+ * Read the current lifecycle phase reactively.
195
+ *
196
+ * Re-renders whenever the phase transitions. Returns `LifecyclePhase.None`
197
+ * before the handshake completes.
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * const phase = useLifecyclePhase();
202
+ * const isPlaying = phase === LifecyclePhase.Start;
203
+ * const isVisible = phase === LifecyclePhase.Show || phase === LifecyclePhase.Start;
204
+ * ```
205
+ */
206
+ export declare function useLifecyclePhase(): LifecyclePhase;
207
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,KAAK,MAAM,EAAgB,MAAM,iBAAiB,CAAC;AA8BlK;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAOjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAOjG;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,QAAQ,EAAE,GAAG,IAAI,CAOhD;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,8FAA8F;IAC9F,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iFAAiF;IACjF,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAiB/F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,IAAI,OAAO,CAkBlC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAkBxC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,IAAI,UAAU,GAAG,IAAI,CAO7C;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAGzC;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,GAAG,IAAI,CAIpE;AAMD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IACpC,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IACnC,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAoChE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAgBlD"}
package/dist/hooks.js ADDED
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Core React hooks for AppStrata.
3
+ *
4
+ * All hooks must be used within an AppStrataProvider.
5
+ */
6
+ import { useContext, useSyncExternalStore, useState, useEffect, useRef, useMemo } from "react";
7
+ import { AppStrataContext } from "./provider.js";
8
+ import { createLogger } from "@appstrata/core";
9
+ import { resolveSourceUri, loadFonts, loadStylesheets } from "@appstrata/utils";
10
+ const logger = createLogger("ReactHooks");
11
+ // ═══════════════════════════════════════════════════════════════════════════
12
+ // INTERNAL HELPER
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ /**
15
+ * Internal hook to access the AppStrata React context.
16
+ * Throws if used outside of AppStrataProvider.
17
+ *
18
+ * @internal
19
+ */
20
+ function useAppStrataContext() {
21
+ const context = useContext(AppStrataContext);
22
+ if (!context) {
23
+ throw new Error("AppStrata hooks must be used within <AppStrataProvider>. " +
24
+ "Wrap your app with <AppStrataProvider> in your root component.");
25
+ }
26
+ return context;
27
+ }
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // CONTEXT HOOKS (useSyncExternalStore-based)
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+ /**
32
+ * Access the complete AppContext.
33
+ * Re-renders on ANY context change.
34
+ *
35
+ * Returns null while waiting for the player READY message.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * const context = useAppContext();
40
+ * if (!context) return <div>Loading...</div>;
41
+ * return <h1>{context.config.title as string}</h1>;
42
+ * ```
43
+ */
44
+ export function useAppContext() {
45
+ const { store } = useAppStrataContext();
46
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
47
+ }
48
+ /**
49
+ * Access just the config portion of AppContext.
50
+ * Only re-renders when config actually changes (deep comparison).
51
+ *
52
+ * @typeParam T - The expected config shape
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const config = useConfig<{ title: string; bgcolor: string }>();
57
+ * if (!config) return null;
58
+ * return <h1 style={{ background: config.bgcolor }}>{config.title}</h1>;
59
+ * ```
60
+ */
61
+ export function useConfig() {
62
+ const { store } = useAppStrataContext();
63
+ return useSyncExternalStore(store.subscribeConfig, store.getConfigSnapshot, store.getConfigSnapshot);
64
+ }
65
+ /**
66
+ * Access just the resources from AppContext.
67
+ * Only re-renders when resources actually change (deep comparison).
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * const resources = useResources();
72
+ * const fonts = resources?.filter(r => r.category === "font");
73
+ * ```
74
+ */
75
+ export function useResources() {
76
+ const { store } = useAppStrataContext();
77
+ return useSyncExternalStore(store.subscribeResources, store.getResourcesSnapshot, store.getResourcesSnapshot);
78
+ }
79
+ /**
80
+ * Look up a single resource by category and name, returning a ready-to-use URI.
81
+ * Only re-renders when resources actually change (deep comparison).
82
+ *
83
+ * Optionally resolves a named variant instead of the main source.
84
+ *
85
+ * @param category - Resource category (e.g. "font", "image")
86
+ * @param name - Resource name (e.g. "heading", "logo")
87
+ * @param variant - Optional variant name (e.g. "thumbnail")
88
+ * @returns Object with `uri`, `source`, and `resource` fields
89
+ *
90
+ * @example
91
+ * ```tsx
92
+ * const { uri: fontUri } = useResource("font", "heading");
93
+ * const { uri: logoUri } = useResource("image", "logo");
94
+ *
95
+ * // With variant:
96
+ * const { uri: thumbUri } = useResource("image", "logo", "thumbnail");
97
+ * ```
98
+ */
99
+ export function useResource(category, name, variant) {
100
+ const resources = useResources();
101
+ return useMemo(() => {
102
+ if (!resources)
103
+ return { uri: null, source: null, resource: null };
104
+ const resource = resources.find(r => r.category === category && r.name === name) ?? null;
105
+ if (!resource)
106
+ return { uri: null, source: null, resource: null };
107
+ const source = variant
108
+ ? (resource.variants?.[variant]?.source ?? null)
109
+ : resource.source;
110
+ const uri = source ? resolveSourceUri(source) : null;
111
+ return { uri, source, resource };
112
+ }, [resources, category, name, variant]);
113
+ }
114
+ /**
115
+ * Load all font resources from the player context.
116
+ *
117
+ * Injects `<link>` stylesheet elements for every resource with
118
+ * `category === "font"`. Re-loads when the resource list changes
119
+ * (e.g. during CMS live preview).
120
+ *
121
+ * @returns `true` once all font stylesheets have loaded, `false` while loading
122
+ *
123
+ * @example
124
+ * ```tsx
125
+ * const fontsLoaded = useFonts();
126
+ * if (!fontsLoaded) return <div>Loading fonts...</div>;
127
+ * ```
128
+ */
129
+ export function useFonts() {
130
+ const resources = useResources();
131
+ const [loaded, setLoaded] = useState(false);
132
+ useEffect(() => {
133
+ if (!resources)
134
+ return;
135
+ const fonts = resources.filter(r => r.category === "font");
136
+ if (fonts.length === 0) {
137
+ setLoaded(true);
138
+ return;
139
+ }
140
+ setLoaded(false);
141
+ loadFonts(resources).then(() => setLoaded(true));
142
+ }, [resources]);
143
+ return loaded;
144
+ }
145
+ /**
146
+ * Load all stylesheet resources from the player context.
147
+ *
148
+ * Injects `<link>` stylesheet elements for every resource with
149
+ * `category === "stylesheet"`. Re-loads when the resource list changes
150
+ * (e.g. during CMS live preview).
151
+ *
152
+ * @returns `true` once all stylesheets have loaded, `false` while loading
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * const stylesLoaded = useStylesheets();
157
+ * if (!stylesLoaded) return <div>Loading styles...</div>;
158
+ * ```
159
+ */
160
+ export function useStylesheets() {
161
+ const resources = useResources();
162
+ const [loaded, setLoaded] = useState(false);
163
+ useEffect(() => {
164
+ if (!resources)
165
+ return;
166
+ const sheets = resources.filter(r => r.category === "stylesheet");
167
+ if (sheets.length === 0) {
168
+ setLoaded(true);
169
+ return;
170
+ }
171
+ setLoaded(false);
172
+ loadStylesheets(resources).then(() => setLoaded(true));
173
+ }, [resources]);
174
+ return loaded;
175
+ }
176
+ /**
177
+ * Access just the device information from AppContext.
178
+ * Only re-renders when device info actually changes (deep comparison).
179
+ *
180
+ * @example
181
+ * ```tsx
182
+ * const device = useDevice();
183
+ * if (!device) return null;
184
+ * return <footer>Playing on: {device.name}</footer>;
185
+ * ```
186
+ */
187
+ export function useDevice() {
188
+ const { store } = useAppStrataContext();
189
+ return useSyncExternalStore(store.subscribeDevice, store.getDeviceSnapshot, store.getDeviceSnapshot);
190
+ }
191
+ // ═══════════════════════════════════════════════════════════════════════════
192
+ // PLAYER ACCESS
193
+ // ═══════════════════════════════════════════════════════════════════════════
194
+ /**
195
+ * Access the SignagePlayer instance directly.
196
+ * Returns a stable reference (singleton) -- never triggers re-renders.
197
+ *
198
+ * Use this for imperative operations or capabilities not covered by
199
+ * dedicated hooks.
200
+ *
201
+ * @example
202
+ * ```tsx
203
+ * const player = usePlayer();
204
+ * const handleComplete = () => player.notifyComplete();
205
+ * ```
206
+ */
207
+ export function usePlayer() {
208
+ const { player } = useAppStrataContext();
209
+ return player;
210
+ }
211
+ // ═══════════════════════════════════════════════════════════════════════════
212
+ // CAPABILITY CHECK
213
+ // ═══════════════════════════════════════════════════════════════════════════
214
+ /**
215
+ * Check if a capability is available.
216
+ *
217
+ * Returns a tri-state value:
218
+ * - `null` while the player hasn't initialized yet (pre-READY)
219
+ * - `true` when the capability is supported
220
+ * - `false` when the capability is NOT supported
221
+ *
222
+ * Re-renders on any context change (delegates to useAppContext internally).
223
+ *
224
+ * @param capability - The capability to check (e.g., "storage", "proxy")
225
+ *
226
+ * @example
227
+ * ```tsx
228
+ * const hasStorage = useCapability("storage");
229
+ * if (hasStorage === null) return <div>Initializing...</div>;
230
+ * if (hasStorage) { ... }
231
+ * ```
232
+ */
233
+ export function useCapability(capability) {
234
+ const context = useAppContext();
235
+ if (context === null)
236
+ return null; // Not yet initialized
237
+ return context.hasCapability(capability);
238
+ }
239
+ /**
240
+ * Subscribe to lifecycle events.
241
+ * Callbacks can optionally return cleanup functions.
242
+ *
243
+ * Auto-subscribes on mount, auto-cleans up on unmount.
244
+ *
245
+ * **Note:** Because `useEffect` runs after paint, handlers for transient phases
246
+ * like `Show` and `Hide` may not fire if the phase has already passed by the
247
+ * time the effect registers them. Use `onStart`/`onStop` for work that must
248
+ * not be skipped, or read `useLifecyclePhase()` to derive state from the
249
+ * current phase.
250
+ *
251
+ * @example
252
+ * ```tsx
253
+ * useLifecycle({
254
+ * onStart: () => {
255
+ * const interval = setInterval(tick, 1000);
256
+ * return () => clearInterval(interval); // cleanup
257
+ * },
258
+ * onStop: () => console.log("Stopped"),
259
+ * });
260
+ * ```
261
+ */
262
+ export function useLifecycle(callbacks) {
263
+ const { player } = useAppStrataContext();
264
+ // Use refs to always call the latest callbacks without re-subscribing
265
+ const callbacksRef = useRef(callbacks);
266
+ callbacksRef.current = callbacks;
267
+ useEffect(() => {
268
+ const cleanups = [];
269
+ player.onShow(() => {
270
+ const cleanup = callbacksRef.current.onShow?.();
271
+ if (typeof cleanup === "function")
272
+ cleanups.push(cleanup);
273
+ });
274
+ player.onStart(() => {
275
+ const cleanup = callbacksRef.current.onStart?.();
276
+ if (typeof cleanup === "function")
277
+ cleanups.push(cleanup);
278
+ });
279
+ player.onHide(() => {
280
+ const cleanup = callbacksRef.current.onHide?.();
281
+ if (typeof cleanup === "function")
282
+ cleanups.push(cleanup);
283
+ });
284
+ player.onStop(() => {
285
+ const cleanup = callbacksRef.current.onStop?.();
286
+ if (typeof cleanup === "function")
287
+ cleanups.push(cleanup);
288
+ });
289
+ logger.info("Lifecycle hooks registered");
290
+ return () => {
291
+ cleanups.forEach((cleanup) => cleanup());
292
+ };
293
+ }, [player]);
294
+ }
295
+ /**
296
+ * Read the current lifecycle phase reactively.
297
+ *
298
+ * Re-renders whenever the phase transitions. Returns `LifecyclePhase.None`
299
+ * before the handshake completes.
300
+ *
301
+ * @example
302
+ * ```tsx
303
+ * const phase = useLifecyclePhase();
304
+ * const isPlaying = phase === LifecyclePhase.Start;
305
+ * const isVisible = phase === LifecyclePhase.Show || phase === LifecyclePhase.Start;
306
+ * ```
307
+ */
308
+ export function useLifecyclePhase() {
309
+ const { player } = useAppStrataContext();
310
+ const [phase, setPhase] = useState(player.lifecyclePhase);
311
+ useEffect(() => {
312
+ // Sync in case the phase changed between render and effect
313
+ setPhase(player.lifecyclePhase);
314
+ player.onInit(() => setPhase(player.lifecyclePhase));
315
+ player.onShow(() => setPhase(player.lifecyclePhase));
316
+ player.onStart(() => setPhase(player.lifecyclePhase));
317
+ player.onHide(() => setPhase(player.lifecyclePhase));
318
+ player.onStop(() => setPhase(player.lifecyclePhase));
319
+ }, [player]);
320
+ return phase;
321
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @appstrata/react - React hooks and components for AppStrata Digital Signage SDK
3
+ *
4
+ * Provides React hooks for app developers to interact with the SignagePlayer runtime.
5
+ * All hooks must be used within an AppStrataProvider.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { AppStrataProvider, useAppContext, useLifecyclePhase, LifecyclePhase } from "@appstrata/react";
10
+ *
11
+ * function App() {
12
+ * const context = useAppContext();
13
+ * const phase = useLifecyclePhase();
14
+ *
15
+ * if (!context) return <div>Loading...</div>;
16
+ * return <>
17
+ * <h1>{context.config.title as string}</h1>
18
+ * <p>Phase: {phase}</p>
19
+ * </>;
20
+ * }
21
+ *
22
+ * root.render(
23
+ * <AppStrataProvider>
24
+ * <App />
25
+ * </AppStrataProvider>
26
+ * );
27
+ * ```
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ export { LifecyclePhase } from "@appstrata/core";
32
+ export { AppStrataProvider } from "./provider.js";
33
+ export type { AppStrataProviderProps } from "./provider.js";
34
+ export { useAppContext, useConfig, useResources, useResource, useFonts, useStylesheets, useDevice, usePlayer, useCapability, useLifecycle, useLifecyclePhase, } from "./hooks.js";
35
+ export type { LifecycleCallbacks, UseResourceResult } from "./hooks.js";
36
+ export { useStorage } from "./use-storage.js";
37
+ export type { StorageFallbackAdapter, UseStorageOptions, UseStorageKeyResult, UseStorageFullResult, } from "./use-storage.js";
38
+ export { useProxy } from "./use-proxy.js";
39
+ export type { UseProxyOptions, UseProxyResult } from "./use-proxy.js";
40
+ export { useMediaCache } from "./use-media-cache.js";
41
+ export type { UseMediaCacheOptions, UseMediaCacheResult } from "./use-media-cache.js";
42
+ export { useStatic } from "./use-static.js";
43
+ export type { UseStaticResult } from "./use-static.js";
44
+ export { useInteraction } from "./use-interaction.js";
45
+ export type { UseInteractionResult } from "./use-interaction.js";
46
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,YAAY,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG5D,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,cAAc,EACd,SAAS,EACT,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGtE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGtF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGvD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @appstrata/react - React hooks and components for AppStrata Digital Signage SDK
3
+ *
4
+ * Provides React hooks for app developers to interact with the SignagePlayer runtime.
5
+ * All hooks must be used within an AppStrataProvider.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { AppStrataProvider, useAppContext, useLifecyclePhase, LifecyclePhase } from "@appstrata/react";
10
+ *
11
+ * function App() {
12
+ * const context = useAppContext();
13
+ * const phase = useLifecyclePhase();
14
+ *
15
+ * if (!context) return <div>Loading...</div>;
16
+ * return <>
17
+ * <h1>{context.config.title as string}</h1>
18
+ * <p>Phase: {phase}</p>
19
+ * </>;
20
+ * }
21
+ *
22
+ * root.render(
23
+ * <AppStrataProvider>
24
+ * <App />
25
+ * </AppStrataProvider>
26
+ * );
27
+ * ```
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ // Re-export LifecyclePhase for convenience (avoids needing @appstrata/core import)
32
+ export { LifecyclePhase } from "@appstrata/core";
33
+ // Provider
34
+ export { AppStrataProvider } from "./provider.js";
35
+ // Core hooks
36
+ export { useAppContext, useConfig, useResources, useResource, useFonts, useStylesheets, useDevice, usePlayer, useCapability, useLifecycle, useLifecyclePhase, } from "./hooks.js";
37
+ // Storage hook
38
+ export { useStorage } from "./use-storage.js";
39
+ // Proxy hook
40
+ export { useProxy } from "./use-proxy.js";
41
+ // Media Cache hook
42
+ export { useMediaCache } from "./use-media-cache.js";
43
+ // Static hook
44
+ export { useStatic } from "./use-static.js";
45
+ // Interaction hook
46
+ export { useInteraction } from "./use-interaction.js";