@flow-like/widget-sdk 0.1.1

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/src/mount.ts ADDED
@@ -0,0 +1,427 @@
1
+ import { createWidgetMediaClient, type WidgetMediaState } from "./media";
2
+ import { type MapStore, type ReadableAtom, atom, map } from "nanostores";
3
+ import {
4
+ CONTRACT_VERSION,
5
+ type WidgetContract,
6
+ contractDefaults,
7
+ } from "./contract";
8
+ import type {
9
+ QueryArgs,
10
+ QueryReturns,
11
+ WidgetDefinition,
12
+ WidgetEventsShape,
13
+ WidgetInputsShape,
14
+ WidgetQueriesShape,
15
+ } from "./define";
16
+ import {
17
+ type FlwEnvelope,
18
+ type FlwMessageType,
19
+ type FlwPayloadMap,
20
+ type InitPayload,
21
+ type InitCapabilities,
22
+ type PropsUpdatePayload,
23
+ type QueryPayload,
24
+ type ThemeState,
25
+ createEnvelope,
26
+ isFlwEnvelope,
27
+ } from "./protocol";
28
+ import {
29
+ detectColorScheme,
30
+ renderStandaloneBadge,
31
+ themeForMode,
32
+ watchColorScheme,
33
+ } from "./standalone";
34
+ import { applyTheme } from "./theme";
35
+ import { validateInputValue, validateSchema } from "./validate";
36
+
37
+ import {
38
+ createWidgetMicrophoneClient,
39
+ type WidgetAudioOptions,
40
+ type WidgetAudioResult,
41
+ } from "./microphone";
42
+ import { dispatchWidgetQuery } from "./query";
43
+
44
+ export const INIT_TIMEOUT_MS = 300;
45
+
46
+ export type BridgeMode = "connecting" | "hosted" | "standalone";
47
+
48
+ export type EmitPayloadArgs<E, Name extends keyof E> = E[Name] extends void
49
+ ? []
50
+ : undefined extends E[Name]
51
+ ? [payload?: E[Name]]
52
+ : [payload: E[Name]];
53
+
54
+ export interface WidgetBridge<
55
+ I extends WidgetInputsShape = Record<string, unknown>,
56
+ E extends WidgetEventsShape = Record<string, unknown>,
57
+ Q extends WidgetQueriesShape = Record<
58
+ string,
59
+ { args: unknown; returns: unknown }
60
+ >,
61
+ > {
62
+ readonly $props: MapStore<I>;
63
+ readonly $theme: ReadableAtom<ThemeState>;
64
+ readonly $mode: ReadableAtom<BridgeMode>;
65
+ readonly $capabilities: ReadableAtom<InitCapabilities>;
66
+ captureAudio(options?: WidgetAudioOptions): Promise<WidgetAudioResult>;
67
+ stopAudioCapture(): void;
68
+ readonly $media: ReadableAtom<WidgetMediaState>;
69
+ playMedia(id: string): Promise<void>;
70
+ pauseMedia(): void;
71
+ stopMedia(): void;
72
+ emit<Name extends keyof E & string>(
73
+ name: Name,
74
+ ...args: EmitPayloadArgs<E, Name>
75
+ ): void;
76
+ onQuery<Name extends keyof Q & string>(
77
+ name: Name,
78
+ handler: (
79
+ args: QueryArgs<Q, Name>,
80
+ ) => QueryReturns<Q, Name> | Promise<QueryReturns<Q, Name>>,
81
+ ): () => void;
82
+ setValues(values: Record<string, unknown>): void;
83
+ dispose(): void;
84
+ }
85
+
86
+ export interface FlwStandaloneGlobal {
87
+ query(name: string, args?: unknown): Promise<unknown>;
88
+ bridge: WidgetBridge;
89
+ }
90
+
91
+ type FlwGlobals = {
92
+ __FLW_CONTRACT__?: WidgetContract;
93
+ __flw?: FlwStandaloneGlobal;
94
+ };
95
+
96
+ export function acceptEnvelope(
97
+ data: unknown,
98
+ expectedNonce: string | null,
99
+ sourceIsParent: boolean,
100
+ ): FlwEnvelope | null {
101
+ if (!sourceIsParent) return null;
102
+ if (!isFlwEnvelope(data)) return null;
103
+ if (expectedNonce === null) return data.type === "init" ? data : null;
104
+ if (data.nonce !== expectedNonce) return null;
105
+ return data;
106
+ }
107
+
108
+ export function mergeInitProps(
109
+ contract: WidgetContract | null | undefined,
110
+ initProps: Record<string, unknown>,
111
+ ): Record<string, unknown> {
112
+ const { publicMediaGrants: _mediaGrants, ...props } = {
113
+ ...contractDefaults(contract),
114
+ ...initProps,
115
+ };
116
+ return props;
117
+ }
118
+
119
+ export interface RejectedPatchEntry {
120
+ key: string;
121
+ errors: string[];
122
+ }
123
+
124
+ export function filterPropsPatch(
125
+ contract: WidgetContract | null | undefined,
126
+ patch: Record<string, unknown>,
127
+ ): { accepted: Record<string, unknown>; rejected: RejectedPatchEntry[] } {
128
+ const accepted: Record<string, unknown> = {};
129
+ const rejected: RejectedPatchEntry[] = [];
130
+ if (!contract) {
131
+ const { publicMediaGrants: _mediaGrants, ...publicPatch } = patch;
132
+ return { accepted: publicPatch, rejected };
133
+ }
134
+ for (const [key, value] of Object.entries(patch)) {
135
+ if (key === "publicMediaGrants") continue;
136
+ const input = contract.inputs?.[key];
137
+ if (!input) {
138
+ rejected.push({ key, errors: ["not declared in the contract"] });
139
+ continue;
140
+ }
141
+ const result = validateInputValue(input, value);
142
+ if (result.valid) {
143
+ accepted[key] = value;
144
+ } else {
145
+ rejected.push({ key, errors: result.errors });
146
+ }
147
+ }
148
+ return { accepted, rejected };
149
+ }
150
+
151
+ export function mountFlowWidget<
152
+ I extends WidgetInputsShape,
153
+ E extends WidgetEventsShape,
154
+ Q extends WidgetQueriesShape,
155
+ >(definition: WidgetDefinition<I, E, Q>): WidgetBridge<I, E, Q> {
156
+ if (typeof window === "undefined") {
157
+ throw new Error(
158
+ "mountFlowWidget requires a browser environment (window is undefined)",
159
+ );
160
+ }
161
+
162
+ const flwGlobals = globalThis as FlwGlobals;
163
+ const contract = flwGlobals.__FLW_CONTRACT__;
164
+
165
+ const $props = map<I>(mergeInitProps(contract, {}) as I);
166
+ const $theme = atom<ThemeState>(themeForMode("light"));
167
+ const $mode = atom<BridgeMode>("connecting");
168
+ const $capabilities = atom<InitCapabilities>({});
169
+
170
+ let nonce: string | null = null;
171
+ let instanceId = "";
172
+ let preview = false;
173
+ let disposed = false;
174
+ const queryHandlers = new Map<string, (args: unknown) => unknown>();
175
+
176
+ let standaloneTimer: ReturnType<typeof setTimeout> | null = null;
177
+ let standaloneCleanup: (() => void) | null = null;
178
+ let resizeObserver: ResizeObserver | null = null;
179
+ let resizeRaf: number | null = null;
180
+
181
+ const post = <T extends FlwMessageType>(
182
+ type: T,
183
+ payload: FlwPayloadMap[T],
184
+ ) => {
185
+ window.parent.postMessage(
186
+ createEnvelope(type, payload, nonce ?? "", instanceId),
187
+ "*",
188
+ );
189
+ };
190
+
191
+ const microphone = createWidgetMicrophoneClient(post, $capabilities.get);
192
+ const media = createWidgetMediaClient(post, $capabilities.get);
193
+
194
+ const setAndApplyTheme = (theme: ThemeState) => {
195
+ $theme.set(theme);
196
+ applyTheme(theme);
197
+ };
198
+
199
+ const startAutoResize = () => {
200
+ if (resizeObserver || typeof ResizeObserver === "undefined") return;
201
+ const resizable =
202
+ contract?.sizing?.resizable ?? definition.sizing?.resizable ?? true;
203
+ if (!resizable) return;
204
+ resizeObserver = new ResizeObserver(() => {
205
+ if (resizeRaf !== null) return;
206
+ resizeRaf = requestAnimationFrame(() => {
207
+ resizeRaf = null;
208
+ post("resize", { height: document.documentElement.scrollHeight });
209
+ });
210
+ });
211
+ resizeObserver.observe(document.documentElement);
212
+ };
213
+
214
+ const cleanupStandalone = () => {
215
+ standaloneCleanup?.();
216
+ standaloneCleanup = null;
217
+ };
218
+
219
+ const handleInit = (envelope: FlwEnvelope<"init">) => {
220
+ const payload = envelope.payload as InitPayload;
221
+ if (standaloneTimer !== null) {
222
+ clearTimeout(standaloneTimer);
223
+ standaloneTimer = null;
224
+ }
225
+ cleanupStandalone();
226
+ microphone.reset();
227
+ media.reset();
228
+ $capabilities.set(payload.capabilities ?? {});
229
+ nonce = envelope.nonce;
230
+ instanceId = payload.instanceId || envelope.instanceId;
231
+ preview = payload.capabilities?.preview === true;
232
+ $props.set(mergeInitProps(contract, payload.props ?? {}) as I);
233
+ setAndApplyTheme(payload.theme);
234
+ post("ready", {
235
+ contractVersion: contract?.contractVersion ?? CONTRACT_VERSION,
236
+ });
237
+ $mode.set("hosted");
238
+ startAutoResize();
239
+ };
240
+
241
+ const handlePropsUpdate = (payload: PropsUpdatePayload) => {
242
+ const { accepted, rejected } = filterPropsPatch(
243
+ contract,
244
+ payload.props ?? {},
245
+ );
246
+ for (const entry of rejected) {
247
+ console.warn(
248
+ `[flow-like/widget-sdk] dropped props:update value for "${entry.key}": ${entry.errors.join("; ")}`,
249
+ );
250
+ }
251
+ const store = $props as MapStore<Record<string, unknown>>;
252
+ for (const [key, value] of Object.entries(accepted)) {
253
+ store.setKey(key, value);
254
+ }
255
+ };
256
+
257
+ const handleQuery = async (payload: QueryPayload) => {
258
+ post(
259
+ "query:result",
260
+ await dispatchWidgetQuery(contract, queryHandlers, payload),
261
+ );
262
+ };
263
+
264
+ const onMessage = (event: MessageEvent) => {
265
+ if (disposed) return;
266
+ const envelope = acceptEnvelope(
267
+ event.data,
268
+ nonce,
269
+ event.source === window.parent,
270
+ );
271
+ if (!envelope) return;
272
+ if (instanceId && envelope.instanceId !== instanceId) return;
273
+ microphone.handle(envelope);
274
+ media.handle(envelope);
275
+ switch (envelope.type) {
276
+ case "capabilities:update":
277
+ $capabilities.set(envelope.payload as InitCapabilities);
278
+ break;
279
+ case "init":
280
+ handleInit(envelope as FlwEnvelope<"init">);
281
+ break;
282
+ case "props:update":
283
+ handlePropsUpdate(envelope.payload as PropsUpdatePayload);
284
+ break;
285
+ case "theme:change":
286
+ setAndApplyTheme(envelope.payload as ThemeState);
287
+ break;
288
+ case "query":
289
+ void handleQuery(envelope.payload as QueryPayload);
290
+ break;
291
+ default:
292
+ break;
293
+ }
294
+ };
295
+
296
+ const emit = <Name extends keyof E & string>(
297
+ name: Name,
298
+ ...args: EmitPayloadArgs<E, Name>
299
+ ) => {
300
+ const payload = (args as unknown[])[0];
301
+ if ($mode.get() !== "hosted") {
302
+ console.info("[flow-like/widget-sdk] event (standalone)", {
303
+ name,
304
+ payload,
305
+ });
306
+ return;
307
+ }
308
+ if (preview) return;
309
+ if (contract) {
310
+ const eventSpec = contract.events?.[name];
311
+ if (!eventSpec) {
312
+ console.warn(
313
+ `[flow-like/widget-sdk] dropped event "${name}": not declared in the contract`,
314
+ );
315
+ return;
316
+ }
317
+ const result = validateSchema(eventSpec.payloadSchema, payload);
318
+ if (!result.valid) {
319
+ console.warn(
320
+ `[flow-like/widget-sdk] dropped event "${name}" with invalid payload: ${result.errors.join("; ")}`,
321
+ );
322
+ return;
323
+ }
324
+ }
325
+ post("event", { name, payload });
326
+ };
327
+
328
+ const onQuery = <Name extends keyof Q & string>(
329
+ name: Name,
330
+ handler: (
331
+ args: QueryArgs<Q, Name>,
332
+ ) => QueryReturns<Q, Name> | Promise<QueryReturns<Q, Name>>,
333
+ ): (() => void) => {
334
+ const untyped = handler as (args: unknown) => unknown;
335
+ queryHandlers.set(name, untyped);
336
+ return () => {
337
+ if (queryHandlers.get(name) === untyped) queryHandlers.delete(name);
338
+ };
339
+ };
340
+
341
+ const setValues = (values: Record<string, unknown>) => {
342
+ if ($mode.get() === "hosted") {
343
+ post("value:changed", { values });
344
+ } else {
345
+ console.info("[flow-like/widget-sdk] value:changed (standalone)", values);
346
+ }
347
+ };
348
+
349
+ const dispose = () => {
350
+ if (disposed) return;
351
+ disposed = true;
352
+ microphone.dispose();
353
+ media.dispose();
354
+ window.removeEventListener("message", onMessage);
355
+ if (standaloneTimer !== null) {
356
+ clearTimeout(standaloneTimer);
357
+ standaloneTimer = null;
358
+ }
359
+ if (resizeRaf !== null) {
360
+ cancelAnimationFrame(resizeRaf);
361
+ resizeRaf = null;
362
+ }
363
+ resizeObserver?.disconnect();
364
+ resizeObserver = null;
365
+ cleanupStandalone();
366
+ queryHandlers.clear();
367
+ };
368
+
369
+ const bridge: WidgetBridge<I, E, Q> = {
370
+ $props,
371
+ $theme,
372
+ $mode,
373
+ $capabilities,
374
+ captureAudio: microphone.captureAudio,
375
+ stopAudioCapture: microphone.stopAudioCapture,
376
+ $media: media.$media,
377
+ playMedia: media.playMedia,
378
+ pauseMedia: media.pauseMedia,
379
+ stopMedia: media.stopMedia,
380
+ emit,
381
+ onQuery,
382
+ setValues,
383
+ dispose,
384
+ };
385
+
386
+ const bootStandalone = () => {
387
+ if (disposed || $mode.get() !== "connecting") return;
388
+ standaloneTimer = null;
389
+ $props.set(mergeInitProps(contract, {}) as I);
390
+ setAndApplyTheme(themeForMode(detectColorScheme()));
391
+ const unwatch = watchColorScheme((mode) => {
392
+ setAndApplyTheme(themeForMode(mode));
393
+ });
394
+ const removeBadge = renderStandaloneBadge();
395
+ const standaloneGlobal: FlwStandaloneGlobal = {
396
+ query: async (name, args) => {
397
+ const result = await dispatchWidgetQuery(contract, queryHandlers, {
398
+ queryId: "standalone",
399
+ name,
400
+ args,
401
+ });
402
+ if (!result.ok) {
403
+ throw new Error(result.error ?? `Query "${name}" failed`);
404
+ }
405
+ return result.value;
406
+ },
407
+ bridge: bridge as unknown as WidgetBridge,
408
+ };
409
+ flwGlobals.__flw = standaloneGlobal;
410
+ standaloneCleanup = () => {
411
+ unwatch();
412
+ removeBadge();
413
+ if (flwGlobals.__flw === standaloneGlobal) flwGlobals.__flw = undefined;
414
+ };
415
+ $mode.set("standalone");
416
+ };
417
+
418
+ window.addEventListener("message", onMessage);
419
+ if (window.self !== window.top) {
420
+ window.parent.postMessage(createEnvelope("hello", {}, "", ""), "*");
421
+ standaloneTimer = setTimeout(bootStandalone, INIT_TIMEOUT_MS);
422
+ } else {
423
+ bootStandalone();
424
+ }
425
+
426
+ return bridge;
427
+ }
@@ -0,0 +1,178 @@
1
+ import type {
2
+ MediaPlayPayload,
3
+ MediaResultPayload,
4
+ WidgetMediaState,
5
+ } from "./media";
6
+ import type {
7
+ AudioRequestPayload,
8
+ MicrophoneResultPayload,
9
+ RequestIdPayload,
10
+ } from "./microphone";
11
+
12
+ export const FLW_PROTOCOL = "flw/1";
13
+
14
+ export type ThemeMode = "light" | "dark";
15
+
16
+ export interface ThemeState {
17
+ mode: ThemeMode;
18
+ tokens: Record<string, string>;
19
+ }
20
+
21
+ export interface InitCapabilities {
22
+ preview?: boolean;
23
+ microphone?: boolean;
24
+ media?: boolean;
25
+ mediaIds?: string[];
26
+ }
27
+
28
+ export interface InitPayload {
29
+ props: Record<string, unknown>;
30
+ theme: ThemeState;
31
+ locale: string;
32
+ instanceId: string;
33
+ capabilities: InitCapabilities;
34
+ }
35
+
36
+ export interface PropsUpdatePayload {
37
+ props: Record<string, unknown>;
38
+ }
39
+
40
+ export interface QueryPayload {
41
+ queryId: string;
42
+ name: string;
43
+ args: unknown;
44
+ }
45
+
46
+ export type HelloPayload = Record<string, never>;
47
+
48
+ export interface ReadyPayload {
49
+ contractVersion: number;
50
+ }
51
+
52
+ export interface EventPayload {
53
+ name: string;
54
+ payload: unknown;
55
+ }
56
+
57
+ export interface QueryResultPayload {
58
+ queryId: string;
59
+ ok: boolean;
60
+ value?: unknown;
61
+ error?: string;
62
+ }
63
+
64
+ export interface ResizePayload {
65
+ height: number;
66
+ }
67
+
68
+ export interface ValueChangedPayload {
69
+ values: Record<string, unknown>;
70
+ }
71
+
72
+ export interface FlwPayloadMap {
73
+ "media:play": MediaPlayPayload;
74
+ "media:pause": Record<string, never>;
75
+ "media:stop": Record<string, never>;
76
+ "media:result": MediaResultPayload;
77
+ "media:state": WidgetMediaState;
78
+ "microphone:request": AudioRequestPayload;
79
+ "microphone:result": MicrophoneResultPayload;
80
+ "microphone:stop": RequestIdPayload;
81
+ "microphone:cancel": RequestIdPayload;
82
+ "capabilities:update": InitCapabilities;
83
+ init: InitPayload;
84
+ "props:update": PropsUpdatePayload;
85
+ "theme:change": ThemeState;
86
+ query: QueryPayload;
87
+ hello: HelloPayload;
88
+ ready: ReadyPayload;
89
+ event: EventPayload;
90
+ "query:result": QueryResultPayload;
91
+ resize: ResizePayload;
92
+ "value:changed": ValueChangedPayload;
93
+ }
94
+
95
+ export type HostToWidgetMessageType =
96
+ | "media:result"
97
+ | "media:state"
98
+ | "microphone:result"
99
+ | "capabilities:update"
100
+ | "init"
101
+ | "props:update"
102
+ | "theme:change"
103
+ | "query";
104
+
105
+ export type WidgetToHostMessageType =
106
+ | "media:play"
107
+ | "media:pause"
108
+ | "media:stop"
109
+ | "microphone:request"
110
+ | "microphone:stop"
111
+ | "microphone:cancel"
112
+ | "hello"
113
+ | "ready"
114
+ | "event"
115
+ | "query:result"
116
+ | "resize"
117
+ | "value:changed";
118
+
119
+ export type FlwMessageType = HostToWidgetMessageType | WidgetToHostMessageType;
120
+
121
+ export interface FlwEnvelope<T extends FlwMessageType = FlwMessageType> {
122
+ protocol: typeof FLW_PROTOCOL;
123
+ nonce: string;
124
+ instanceId: string;
125
+ type: T;
126
+ payload: T extends keyof FlwPayloadMap ? FlwPayloadMap[T] : unknown;
127
+ }
128
+
129
+ const FLW_MESSAGE_TYPES: ReadonlySet<string> = new Set([
130
+ "media:play",
131
+ "media:pause",
132
+ "media:stop",
133
+ "media:result",
134
+ "media:state",
135
+ "microphone:request",
136
+ "microphone:result",
137
+ "microphone:stop",
138
+ "microphone:cancel",
139
+ "capabilities:update",
140
+ "init",
141
+ "props:update",
142
+ "theme:change",
143
+ "query",
144
+ "hello",
145
+ "ready",
146
+ "event",
147
+ "query:result",
148
+ "resize",
149
+ "value:changed",
150
+ ]);
151
+
152
+ export function isFlwEnvelope(value: unknown): value is FlwEnvelope {
153
+ if (typeof value !== "object" || value === null) return false;
154
+ const candidate = value as Record<string, unknown>;
155
+ return (
156
+ candidate.protocol === FLW_PROTOCOL &&
157
+ typeof candidate.nonce === "string" &&
158
+ typeof candidate.instanceId === "string" &&
159
+ typeof candidate.type === "string" &&
160
+ FLW_MESSAGE_TYPES.has(candidate.type) &&
161
+ "payload" in candidate
162
+ );
163
+ }
164
+
165
+ export function createEnvelope<T extends FlwMessageType>(
166
+ type: T,
167
+ payload: FlwPayloadMap[T],
168
+ nonce: string,
169
+ instanceId: string,
170
+ ): FlwEnvelope<T> {
171
+ return {
172
+ protocol: FLW_PROTOCOL,
173
+ nonce,
174
+ instanceId,
175
+ type,
176
+ payload,
177
+ } as FlwEnvelope<T>;
178
+ }
package/src/query.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { WidgetContract } from "./contract";
2
+ import type { QueryPayload, QueryResultPayload } from "./protocol";
3
+ import { validateSchema } from "./validate";
4
+
5
+ export type WidgetQueryHandler = (args: unknown) => unknown;
6
+
7
+ /**
8
+ * Invoke one registered query handler. Read queries retain their permissive
9
+ * behavior; mutation schemas are enforced at the side-effect boundary.
10
+ */
11
+ export async function dispatchWidgetQuery(
12
+ contract: WidgetContract | null | undefined,
13
+ handlers: ReadonlyMap<string, WidgetQueryHandler>,
14
+ payload: QueryPayload,
15
+ ): Promise<QueryResultPayload> {
16
+ const handler = handlers.get(payload.name);
17
+ if (!handler) {
18
+ return {
19
+ queryId: payload.queryId,
20
+ ok: false,
21
+ error: `Unknown query "${payload.name}"`,
22
+ };
23
+ }
24
+
25
+ const query = contract?.queries?.[payload.name];
26
+ const mutation = query?.mutation === true;
27
+ if (mutation) {
28
+ const validation = validateSchema(query.argsSchema, payload.args);
29
+ if (!validation.valid) {
30
+ return {
31
+ queryId: payload.queryId,
32
+ ok: false,
33
+ error: `Invalid arguments for mutation "${payload.name}": ${validation.errors.join("; ")}`,
34
+ };
35
+ }
36
+ }
37
+
38
+ try {
39
+ const value = await handler(payload.args);
40
+ if (mutation) {
41
+ const validation = validateSchema(query.resultSchema, value);
42
+ if (!validation.valid) {
43
+ return {
44
+ queryId: payload.queryId,
45
+ ok: false,
46
+ error: `Invalid result from mutation "${payload.name}": ${validation.errors.join("; ")}`,
47
+ };
48
+ }
49
+ }
50
+ return { queryId: payload.queryId, ok: true, value };
51
+ } catch (error) {
52
+ return {
53
+ queryId: payload.queryId,
54
+ ok: false,
55
+ error: error instanceof Error ? error.message : String(error),
56
+ };
57
+ }
58
+ }
package/src/react.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { useStore } from "@nanostores/react";
2
+ import type {
3
+ WidgetEventsShape,
4
+ WidgetInputsShape,
5
+ WidgetQueriesShape,
6
+ } from "./define";
7
+ import type { BridgeMode, WidgetBridge } from "./mount";
8
+ import type { ThemeState } from "./protocol";
9
+
10
+ export function useWidgetProps<
11
+ I extends WidgetInputsShape,
12
+ E extends WidgetEventsShape,
13
+ Q extends WidgetQueriesShape,
14
+ >(bridge: WidgetBridge<I, E, Q>): I {
15
+ return useStore(bridge.$props);
16
+ }
17
+
18
+ export function useWidgetTheme<
19
+ I extends WidgetInputsShape,
20
+ E extends WidgetEventsShape,
21
+ Q extends WidgetQueriesShape,
22
+ >(bridge: WidgetBridge<I, E, Q>): ThemeState {
23
+ return useStore(bridge.$theme);
24
+ }
25
+
26
+ export function useWidgetMode<
27
+ I extends WidgetInputsShape,
28
+ E extends WidgetEventsShape,
29
+ Q extends WidgetQueriesShape,
30
+ >(bridge: WidgetBridge<I, E, Q>): BridgeMode {
31
+ return useStore(bridge.$mode);
32
+ }
33
+
34
+ export { useStore };